Skip to content

Commit 64eb0f8

Browse files
committed
Expose metrics for prometheus on /api/metrics
1 parent 7ccb609 commit 64eb0f8

File tree

9 files changed

+124
-2
lines changed

9 files changed

+124
-2
lines changed

apps/api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@nestjs/websockets": "^10.3.8",
3333
"@prisma/client": "^6.2.1",
3434
"@types/postmark": "^2.0.3",
35+
"@willsoto/nestjs-prometheus": "^6.0.2",
3536
"bcrypt": "^5.1.1",
3637
"class-transformer": "^0.5.1",
3738
"class-validator": "0.13.2",
@@ -45,6 +46,7 @@
4546
"passport": "^0.7.0",
4647
"passport-jwt": "^4.0.1",
4748
"postmark": "^4.0.5",
49+
"prom-client": "^15.1.3",
4850
"react-dropzone": "^14.2.3",
4951
"reflect-metadata": "^0.1.13",
5052
"rxjs": "^7.8.1",

apps/api/src/app.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { FrequentlyAskedQuestionModule } from './frequently-asked-question/frequ
1818
import { InterestModule } from './interest/interest.module';
1919
import { JobModule } from './job/job.module';
2020
import { LeaderboardModule } from './leaderboard/leaderboard.module';
21+
import { MetricsModule } from './metrics/metrics.module';
2122
import { NotificationModule } from './notification/notification.module';
2223
import { PotentialSponsorModule } from './potential-sponsor/potential-sponsor.module';
2324
import { PrinterModule } from './printer/printer.module';
@@ -30,7 +31,6 @@ import { SponsorContractModule } from './sponsor-contract/sponsor-contract.modul
3031
import { SponsorMaterialsModule } from './sponsor-materials/sponsor-materials.module';
3132
import { SurveyQuestionModule } from './survey-question/survey-question.module';
3233
import { UserModule } from './user/user.module';
33-
3434
@Module({
3535
imports: [
3636
ScheduleModule.forRoot(),
@@ -52,6 +52,7 @@ import { UserModule } from './user/user.module';
5252
PotentialSponsorModule,
5353
SponsorMaterialsModule,
5454
SponsorContractModule,
55+
MetricsModule,
5556

5657
...(process.env.NODE_ENV !== 'dev'
5758
? [

apps/api/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const run = async (app: INestApplication) => {
7979

8080
async function bootstrap() {
8181
const app = await NestFactory.create(AppModule);
82+
8283
app.setGlobalPrefix('api');
8384

8485
setupClassValidator(app);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
CallHandler,
3+
ExecutionContext,
4+
Injectable,
5+
NestInterceptor,
6+
} from '@nestjs/common';
7+
import { InjectMetric } from '@willsoto/nestjs-prometheus';
8+
import { Counter, Histogram } from 'prom-client';
9+
import { tap } from 'rxjs/operators';
10+
11+
@Injectable()
12+
export class MetricsInterceptor implements NestInterceptor {
13+
constructor(
14+
@InjectMetric('http_request_duration_seconds')
15+
public readonly histogram: Histogram<string>,
16+
17+
@InjectMetric('http_error_total')
18+
public readonly errorCounter: Counter<string>,
19+
) {}
20+
21+
intercept(context: ExecutionContext, next: CallHandler) {
22+
const start = process.hrtime();
23+
24+
return next.handle().pipe(
25+
tap(() => {
26+
const req = context.switchToHttp().getRequest();
27+
const res = context.switchToHttp().getResponse();
28+
29+
if (!req || !res) return;
30+
31+
const diff = process.hrtime(start);
32+
const seconds = diff[0] + diff[1] / 1e9;
33+
34+
this.histogram
35+
.labels(
36+
req.method,
37+
req.route?.path ?? 'unknown',
38+
String(res.statusCode),
39+
)
40+
.observe(seconds);
41+
}),
42+
);
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Module } from '@nestjs/common';
2+
import { APP_INTERCEPTOR } from '@nestjs/core'; // <--- Import this
3+
import { PrometheusModule as WPrometheusModule } from '@willsoto/nestjs-prometheus';
4+
5+
import { MetricsInterceptor } from './metrics.interceptor';
6+
import { HttpErrorCounter } from './providers/http-error.counter';
7+
import { HttpRequestDuration } from './providers/http-request.histogram';
8+
9+
@Module({
10+
imports: [
11+
WPrometheusModule.register({
12+
path: '/metrics',
13+
defaultMetrics: {
14+
enabled: true,
15+
},
16+
}),
17+
],
18+
providers: [
19+
HttpRequestDuration,
20+
MetricsInterceptor,
21+
HttpErrorCounter,
22+
{
23+
provide: APP_INTERCEPTOR,
24+
useClass: MetricsInterceptor,
25+
},
26+
],
27+
exports: [WPrometheusModule, HttpRequestDuration, MetricsInterceptor],
28+
})
29+
export class MetricsModule {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { makeCounterProvider } from '@willsoto/nestjs-prometheus';
2+
3+
export const HttpErrorCounter = makeCounterProvider({
4+
name: 'http_error_total',
5+
help: 'Total HTTP errors',
6+
labelNames: ['method', 'route', 'status'],
7+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { makeHistogramProvider } from '@willsoto/nestjs-prometheus';
2+
3+
export const HttpRequestDuration = makeHistogramProvider({
4+
name: 'http_request_duration_seconds',
5+
help: 'Request duration in seconds',
6+
labelNames: ['method', 'route', 'status'],
7+
buckets: [0.05, 0.1, 0.3, 1, 3, 5],
8+
});

infrastructure/ansible/playbooks/api/roles/api/tasks/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
- name: traefiknet
2323
labels:
2424
traefik.enable: 'true'
25-
traefik.http.routers.api.rule: 'Host(`{{ api_domain }}`)'
25+
traefik.http.routers.api.rule: 'Host(`{{ api_domain }}`) && !PathPrefix(`/api/metrics`)'
2626
traefik.http.middlewares.api-retry.retry.attempts: '5'
2727
traefik.http.middlewares.api-retry.retry.initialinterval: '100ms'
2828
traefik.http.middlewares.api-cors.headers.accesscontrolallowmethods: '*'

yarn.lock

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1905,6 +1905,11 @@
19051905
consola "^2.15.0"
19061906
node-fetch "^2.6.1"
19071907

1908+
"@opentelemetry/api@^1.4.0":
1909+
version "1.9.0"
1910+
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
1911+
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
1912+
19081913
19091914
version "2.5.1"
19101915
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
@@ -3592,6 +3597,11 @@
35923597
"@webassemblyjs/ast" "1.14.1"
35933598
"@xtuc/long" "4.2.2"
35943599

3600+
"@willsoto/nestjs-prometheus@^6.0.2":
3601+
version "6.0.2"
3602+
resolved "https://registry.yarnpkg.com/@willsoto/nestjs-prometheus/-/nestjs-prometheus-6.0.2.tgz#d603764a923848442ed092411716c0bf211de01f"
3603+
integrity sha512-ePyLZYdIrOOdlOWovzzMisIgviXqhPVzFpSMKNNhn6xajhRHeBsjAzSdpxZTc6pnjR9hw1lNAHyKnKl7lAPaVg==
3604+
35953605
"@xtuc/ieee754@^1.2.0":
35963606
version "1.2.0"
35973607
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -3940,6 +3950,11 @@ binary-extensions@^2.0.0:
39403950
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
39413951
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
39423952

3953+
3954+
version "1.0.2"
3955+
resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8"
3956+
integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==
3957+
39433958
bl@^4.1.0:
39443959
version "4.1.0"
39453960
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@@ -7541,6 +7556,14 @@ prisma@^6.2.1:
75417556
"@prisma/config" "6.18.0"
75427557
"@prisma/engines" "6.18.0"
75437558

7559+
prom-client@^15.1.3:
7560+
version "15.1.3"
7561+
resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2"
7562+
integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==
7563+
dependencies:
7564+
"@opentelemetry/api" "^1.4.0"
7565+
tdigest "^0.1.1"
7566+
75447567
prompts@^2.0.1:
75457568
version "2.4.2"
75467569
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
@@ -8419,6 +8442,13 @@ tar@^6.1.11:
84198442
mkdirp "^1.0.3"
84208443
yallist "^4.0.0"
84218444

8445+
tdigest@^0.1.1:
8446+
version "0.1.2"
8447+
resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced"
8448+
integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==
8449+
dependencies:
8450+
bintrees "1.0.2"
8451+
84228452
terser-webpack-plugin@^5.3.10:
84238453
version "5.3.14"
84248454
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06"

0 commit comments

Comments
 (0)