Skip to content

Commit d4b8f82

Browse files
CopilotneSpeccgithub-actions[bot]
authored
chore(devops): set up basic prometheus metrics (#541)
* Initial plan * Initial plan for Prometheus metrics integration Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> * Add Prometheus metrics integration with prom-client Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> * Add metrics integration tests and documentation Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> * Bump version up to 1.1.42 * Remove package-lock.json and add to .gitignore (use yarn) Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> * Fix yarn.lock to preserve git dependency commit hash Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> * Update Dockerfile.dev and CI workflows to use Node 16 for prom-client compatibility Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 9fbcf70 commit d4b8f82

13 files changed

Lines changed: 356 additions & 4 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

.github/workflows/integration-tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ jobs:
1010
steps:
1111
- uses: actions/checkout@v2
1212

13+
- name: Use Node.js
14+
uses: actions/setup-node@v3
15+
with:
16+
node-version-file: '.nvmrc'
17+
1318
- name: Run tests
1419
run: yarn test:integration

.github/workflows/tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ jobs:
99
runs-on: ubuntu-22.04
1010
steps:
1111
- uses: actions/checkout@v2
12+
- name: Use Node.js
13+
uses: actions/setup-node@v3
14+
with:
15+
node-version-file: '.nvmrc'
1216
- name: Install modules
1317
run: yarn
1418
- name: Run tests

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ uploads
77
globalConfig.json
88
coverage
99
tls
10+
package-lock.json

docker/Dockerfile.dev

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
FROM node:14.17.0-alpine as builder
1+
FROM node:16-alpine as builder
22

33
WORKDIR /usr/src/app
4-
RUN apk add --no-cache git gcc g++ python make musl-dev
4+
RUN apk add --no-cache git gcc g++ python3 make musl-dev
55

66
COPY package.json yarn.lock ./
77

88
RUN yarn install
99

10-
FROM node:14.17.0-alpine
10+
FROM node:16-alpine
1111

1212
WORKDIR /usr/src/app
1313

docs/METRICS.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Prometheus Metrics
2+
3+
This application exposes Prometheus-compatible metrics on a separate port from the main API server.
4+
5+
## Configuration
6+
7+
The metrics server runs on a separate port configured via the `METRICS_PORT` environment variable:
8+
9+
```bash
10+
# Default: 9090
11+
METRICS_PORT=9090
12+
```
13+
14+
Add this to your `.env` file. See `.env.sample` for reference.
15+
16+
## Metrics Endpoint
17+
18+
The metrics are served at:
19+
20+
```
21+
http://localhost:9090/metrics
22+
```
23+
24+
(Replace `9090` with your configured `METRICS_PORT` if different)
25+
26+
## Available Metrics
27+
28+
### Default Node.js Metrics
29+
30+
The following default Node.js metrics are automatically collected:
31+
32+
- **nodejs_version_info** - Node.js version information
33+
- **process_cpu_user_seconds_total** - Total user CPU time spent in seconds
34+
- **process_cpu_system_seconds_total** - Total system CPU time spent in seconds
35+
- **nodejs_heap_size_total_bytes** - Total heap size in bytes
36+
- **nodejs_heap_size_used_bytes** - Used heap size in bytes
37+
- **nodejs_external_memory_bytes** - External memory in bytes
38+
- **nodejs_heap_space_size_total_bytes** - Total heap space size in bytes
39+
- **nodejs_heap_space_size_used_bytes** - Used heap space size in bytes
40+
- **nodejs_eventloop_lag_seconds** - Event loop lag in seconds
41+
- **nodejs_eventloop_lag_min_seconds** - Minimum event loop lag
42+
- **nodejs_eventloop_lag_max_seconds** - Maximum event loop lag
43+
- **nodejs_eventloop_lag_mean_seconds** - Mean event loop lag
44+
- **nodejs_eventloop_lag_stddev_seconds** - Standard deviation of event loop lag
45+
- **nodejs_eventloop_lag_p50_seconds** - 50th percentile event loop lag
46+
- **nodejs_eventloop_lag_p90_seconds** - 90th percentile event loop lag
47+
- **nodejs_eventloop_lag_p99_seconds** - 99th percentile event loop lag
48+
49+
### Custom HTTP Metrics
50+
51+
#### http_request_duration_seconds (Histogram)
52+
53+
Duration of HTTP requests in seconds, labeled by:
54+
- `method` - HTTP method (GET, POST, etc.)
55+
- `route` - Request route/path
56+
- `status_code` - HTTP status code
57+
58+
Buckets: 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10 seconds
59+
60+
#### http_requests_total (Counter)
61+
62+
Total number of HTTP requests, labeled by:
63+
- `method` - HTTP method (GET, POST, etc.)
64+
- `route` - Request route/path
65+
- `status_code` - HTTP status code
66+
67+
## Testing
68+
69+
### Manual Testing
70+
71+
You can test the metrics endpoint using curl:
72+
73+
```bash
74+
curl http://localhost:9090/metrics
75+
```
76+
77+
Or run the provided test script:
78+
79+
```bash
80+
./test-metrics.sh
81+
```
82+
83+
### Integration Tests
84+
85+
Integration tests for metrics are located in `test/integration/cases/metrics.test.ts`.
86+
87+
Run them with:
88+
89+
```bash
90+
npm run test:integration
91+
```
92+
93+
## Implementation Details
94+
95+
The metrics implementation uses the `prom-client` library and consists of:
96+
97+
1. **Metrics Module** (`src/metrics/index.ts`):
98+
- Initializes a Prometheus registry
99+
- Configures default Node.js metrics collection
100+
- Defines custom HTTP metrics (duration histogram and request counter)
101+
- Provides middleware for tracking HTTP requests
102+
- Creates a separate Express app for serving metrics
103+
104+
2. **Integration** (`src/index.ts`):
105+
- Adds metrics middleware to the main Express app
106+
- Starts metrics server on a separate port
107+
- Keeps metrics server isolated from main API traffic
108+
109+
## Prometheus Configuration
110+
111+
To scrape these metrics with Prometheus, add the following to your `prometheus.yml`:
112+
113+
```yaml
114+
scrape_configs:
115+
- job_name: 'hawk-api'
116+
static_configs:
117+
- targets: ['localhost:9090']
118+
```
119+
120+
Adjust the target host and port according to your deployment.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.1.41",
3+
"version": "1.1.42",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {
@@ -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
*/

0 commit comments

Comments
 (0)