feat(metrics): add Prometheus metrics to all backends

- Add metrics module to calendar, chat, clock, contacts backends
- Add metrics module to mana-core-auth service
- Expose /metrics endpoint for Prometheus scraping
- Track HTTP requests, response times, and custom business metrics

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-26 09:35:01 +01:00
parent 75ffd504bc
commit 1c5a1b8442
35 changed files with 611 additions and 14 deletions

View file

@ -31,6 +31,7 @@
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",
"prom-client": "^15.1.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},

View file

@ -7,6 +7,7 @@ import { AlarmModule } from './alarm/alarm.module';
import { TimerModule } from './timer/timer.module';
import { WorldClockModule } from './world-clock/world-clock.module';
import { PresetModule } from './preset/preset.module';
import { MetricsModule } from './metrics';
@Module({
imports: [
@ -15,6 +16,7 @@ import { PresetModule } from './preset/preset.module';
envFilePath: '.env',
}),
ScheduleModule.forRoot(),
MetricsModule,
DatabaseModule,
HealthModule,
AlarmModule,

View file

@ -1,10 +1,48 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { AppModule } from './app.module';
import { MetricsService } from './metrics/metrics.service';
// Normalize route paths to prevent high cardinality
function normalizeRoute(path: string): string {
return path
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id')
.replace(/\/\d+/g, '/:id');
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Get MetricsService for request tracking
const metricsService = app.get(MetricsService);
// Global Express middleware to track ALL HTTP requests
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.path === '/metrics') {
return next();
}
const startTime = Date.now();
const method = req.method;
const route = normalizeRoute(req.path);
res.once('finish', () => {
const duration = (Date.now() - startTime) / 1000;
metricsService.httpRequestsTotal.inc({
method,
route,
status: res.statusCode.toString(),
});
metricsService.httpRequestDuration.observe(
{ method, route, status: res.statusCode.toString() },
duration
);
});
next();
});
// Enable CORS for mobile and web apps
const corsOrigins = process.env.CORS_ORIGINS?.split(',').map((origin) => origin.trim()) || [
'http://localhost:3000',
@ -30,8 +68,10 @@ async function bootstrap() {
})
);
// Set global prefix for API routes
app.setGlobalPrefix('api/v1');
// Set global prefix for API routes (exclude metrics endpoint)
app.setGlobalPrefix('api/v1', {
exclude: ['metrics', 'health'],
});
const port = process.env.PORT || 3017;
await app.listen(port);

View file

@ -0,0 +1,3 @@
export * from './metrics.module';
export * from './metrics.service';
export * from './metrics.controller';

View file

@ -0,0 +1,13 @@
import { Controller, Get, Header } from '@nestjs/common';
import { MetricsService } from './metrics.service';
@Controller()
export class MetricsController {
constructor(private readonly metricsService: MetricsService) {}
@Get('metrics')
@Header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8')
async getMetrics(): Promise<string> {
return this.metricsService.getMetrics();
}
}

View file

@ -0,0 +1,11 @@
import { Module, Global } from '@nestjs/common';
import { MetricsService } from './metrics.service';
import { MetricsController } from './metrics.controller';
@Global()
@Module({
controllers: [MetricsController],
providers: [MetricsService],
exports: [MetricsService],
})
export class MetricsModule {}

View file

@ -0,0 +1,50 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import * as client from 'prom-client';
@Injectable()
export class MetricsService implements OnModuleInit {
private readonly register: client.Registry;
// HTTP metrics
readonly httpRequestsTotal: client.Counter<string>;
readonly httpRequestDuration: client.Histogram<string>;
constructor() {
this.register = new client.Registry();
// Add default metrics (CPU, memory, event loop, etc.)
client.collectDefaultMetrics({
register: this.register,
prefix: 'clock_',
});
// HTTP request counter
this.httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'],
registers: [this.register],
});
// HTTP request duration histogram
this.httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5],
registers: [this.register],
});
}
onModuleInit() {
// Metrics are ready
}
async getMetrics(): Promise<string> {
return this.register.metrics();
}
getContentType(): string {
return this.register.contentType;
}
}