Skip to content

Commit 0e4005f

Browse files
CopilotneSpecc
andcommitted
Add Prometheus metrics integration with prom-client
Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com>
1 parent 4b1b6db commit 0e4005f

7 files changed

Lines changed: 165 additions & 0 deletions

File tree

.env.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# API server port
22
PORT=4000
33

4+
# Metrics server port
5+
METRICS_PORT=9090
6+
47
# Hawk API database URL
58
MONGO_HAWK_DB_URL=mongodb://mongodb:27017/hawk
69

package-lock.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"mime-types": "^2.1.25",
8181
"mongodb": "^3.7.3",
8282
"morgan": "^1.10.1",
83+
"prom-client": "^15.1.3",
8384
"safe-regex": "^2.1.0",
8485
"ts-node-dev": "^2.0.0",
8586
"uuid": "^8.3.2"

src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import BusinessOperationsFactory from './models/businessOperationsFactory';
2727
import schema from './schema';
2828
import { graphqlUploadExpress } from 'graphql-upload';
2929
import morgan from 'morgan';
30+
import { metricsMiddleware, createMetricsServer } from './metrics';
3031

3132
/**
3233
* Option to enable playground
@@ -48,6 +49,11 @@ class HawkAPI {
4849
*/
4950
private serverPort = +(process.env.PORT || 4000);
5051

52+
/**
53+
* Port to serve metrics endpoint
54+
*/
55+
private metricsPort = +(process.env.METRICS_PORT || 9090);
56+
5157
/**
5258
* Express application
5359
*/
@@ -86,6 +92,11 @@ class HawkAPI {
8692
*/
8793
this.app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
8894

95+
/**
96+
* Add metrics middleware to track HTTP requests
97+
*/
98+
this.app.use(metricsMiddleware);
99+
89100
this.app.use(express.json());
90101
this.app.use(bodyParser.urlencoded({ extended: false }));
91102
this.app.use('/static', express.static(`./static`));
@@ -241,6 +252,15 @@ class HawkAPI {
241252
this.app.use(graphqlUploadExpress());
242253
this.server.applyMiddleware({ app: this.app });
243254

255+
// Start metrics server on separate port
256+
const metricsApp = createMetricsServer();
257+
258+
metricsApp.listen(this.metricsPort, () => {
259+
console.log(
260+
`📊 Metrics server ready at http://localhost:${this.metricsPort}/metrics`
261+
);
262+
});
263+
244264
return new Promise((resolve) => {
245265
this.httpServer.listen({ port: this.serverPort }, () => {
246266
console.log(

src/metrics/index.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import client from 'prom-client';
2+
import express from 'express';
3+
4+
/**
5+
* Create a Registry to register the metrics
6+
*/
7+
const register = new client.Registry();
8+
9+
/**
10+
* Add default Node.js metrics (CPU, memory, event loop, etc.)
11+
*/
12+
client.collectDefaultMetrics({ register });
13+
14+
/**
15+
* HTTP request duration histogram
16+
* Tracks request duration by route, method, and status code
17+
*/
18+
const httpRequestDuration = new client.Histogram({
19+
name: 'http_request_duration_seconds',
20+
help: 'Duration of HTTP requests in seconds',
21+
labelNames: ['method', 'route', 'status_code'],
22+
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
23+
registers: [ register ],
24+
});
25+
26+
/**
27+
* HTTP request counter
28+
* Tracks count of HTTP requests by route, method, and status code
29+
*/
30+
const httpRequestCounter = new client.Counter({
31+
name: 'http_requests_total',
32+
help: 'Total number of HTTP requests',
33+
labelNames: ['method', 'route', 'status_code'],
34+
registers: [ register ],
35+
});
36+
37+
/**
38+
* Express middleware to track HTTP metrics
39+
*/
40+
export function metricsMiddleware(req: express.Request, res: express.Response, next: express.NextFunction): void {
41+
const start = Date.now();
42+
43+
// Hook into response finish event to capture metrics
44+
res.on('finish', () => {
45+
const duration = (Date.now() - start) / 1000; // Convert to seconds
46+
const route = req.route ? req.route.path : req.path;
47+
const method = req.method;
48+
const statusCode = res.statusCode.toString();
49+
50+
// Record metrics
51+
httpRequestDuration.labels(method, route, statusCode).observe(duration);
52+
httpRequestCounter.labels(method, route, statusCode).inc();
53+
});
54+
55+
next();
56+
}
57+
58+
/**
59+
* Create metrics server
60+
* @returns Express application serving metrics endpoint
61+
*/
62+
export function createMetricsServer(): express.Application {
63+
const metricsApp = express();
64+
65+
metricsApp.get('/metrics', async (req, res) => {
66+
res.setHeader('Content-Type', register.contentType);
67+
const metrics = await register.metrics();
68+
69+
res.send(metrics);
70+
});
71+
72+
return metricsApp;
73+
}

src/types/env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ declare namespace NodeJS {
1111
*/
1212
PORT: string;
1313

14+
/**
15+
* Metrics server port
16+
*/
17+
METRICS_PORT: string;
18+
1419
/**
1520
* MongoDB url
1621
*/

yarn.lock

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,11 @@
718718
resolved "https://registry.npmjs.org/@n1ru4l/json-patch-plus/-/json-patch-plus-0.2.0.tgz"
719719
integrity sha512-pLkJy83/rVfDTyQgDSC8GeXAHEdXNHGNJrB1b7wAyGQu0iv7tpMXntKVSqj0+XKNVQbco40SZffNfVALzIt0SQ==
720720

721+
"@opentelemetry/api@^1.4.0":
722+
version "1.9.0"
723+
resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz"
724+
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
725+
721726
"@phc/format@^1.0.0":
722727
version "1.0.0"
723728
resolved "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz"
@@ -1763,6 +1768,11 @@ binary-extensions@^2.0.0:
17631768
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
17641769
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
17651770

1771+
bintrees@1.0.2:
1772+
version "1.0.2"
1773+
resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz"
1774+
integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==
1775+
17661776
bitsyntax@~0.1.0:
17671777
version "0.1.0"
17681778
resolved "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.1.0.tgz"
@@ -5448,6 +5458,14 @@ progress@^2.0.0:
54485458
resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
54495459
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
54505460

5461+
prom-client@^15.1.3:
5462+
version "15.1.3"
5463+
resolved "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz"
5464+
integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==
5465+
dependencies:
5466+
"@opentelemetry/api" "^1.4.0"
5467+
tdigest "^0.1.1"
5468+
54515469
promise-breaker@^5.0.0:
54525470
version "5.0.0"
54535471
resolved "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz"
@@ -6371,6 +6389,13 @@ tar@^6.1.11:
63716389
mkdirp "^1.0.3"
63726390
yallist "^4.0.0"
63736391

6392+
tdigest@^0.1.1:
6393+
version "0.1.2"
6394+
resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz"
6395+
integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==
6396+
dependencies:
6397+
bintrees "1.0.2"
6398+
63746399
terminal-link@^2.0.0:
63756400
version "2.1.1"
63766401
resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz"

0 commit comments

Comments
 (0)