From b2108c192b7e930a7a84fec42ffc4c73819ee9d8 Mon Sep 17 00:00:00 2001 From: tebrihk Date: Thu, 23 Apr 2026 07:55:54 +0100 Subject: [PATCH] Metrics Endpoint --- .../metrics/metrics-collection.service.ts | 95 +++++++++++++++- src/monitoring/monitoring.controller.ts | 103 +++++++++++++++++- 2 files changed, 195 insertions(+), 3 deletions(-) diff --git a/src/monitoring/metrics/metrics-collection.service.ts b/src/monitoring/metrics/metrics-collection.service.ts index 47f35898..c143d274 100644 --- a/src/monitoring/metrics/metrics-collection.service.ts +++ b/src/monitoring/metrics/metrics-collection.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; -import { Registry, collectDefaultMetrics, Histogram, Gauge } from 'prom-client'; +import { Registry, collectDefaultMetrics, Histogram, Gauge, Counter } from 'prom-client'; @Injectable() export class MetricsCollectionService implements OnModuleInit { @@ -7,6 +7,13 @@ export class MetricsCollectionService implements OnModuleInit { public httpRequestDuration: Histogram; public dbQueryDuration: Histogram; public activeConnections: Gauge; + public userRegistrations: Counter; + public assessmentCompletions: Counter; + public learningPathProgress: Gauge; + public cacheHitRate: Gauge; + public queueProcessingTime: Histogram; + public emailCampaignsSent: Counter; + public backupOperations: Counter; constructor() { this.registry = new Registry(); @@ -35,6 +42,63 @@ export class MetricsCollectionService implements OnModuleInit { help: 'Number of active connections', registers: [this.registry], }); + + // User Registrations Counter + this.userRegistrations = new Counter({ + name: 'user_registrations_total', + help: 'Total number of user registrations', + labelNames: ['user_type', 'source'], + registers: [this.registry], + }); + + // Assessment Completions Counter + this.assessmentCompletions = new Counter({ + name: 'assessment_completions_total', + help: 'Total number of assessment completions', + labelNames: ['assessment_type', 'difficulty'], + registers: [this.registry], + }); + + // Learning Path Progress Gauge + this.learningPathProgress = new Gauge({ + name: 'learning_path_progress_percentage', + help: 'Average learning path progress percentage', + labelNames: ['path_id', 'user_id'], + registers: [this.registry], + }); + + // Cache Hit Rate Gauge + this.cacheHitRate = new Gauge({ + name: 'cache_hit_rate_percentage', + help: 'Cache hit rate percentage', + labelNames: ['cache_type'], + registers: [this.registry], + }); + + // Queue Processing Time Histogram + this.queueProcessingTime = new Histogram({ + name: 'queue_processing_duration_seconds', + help: 'Duration of queue job processing in seconds', + labelNames: ['queue_name', 'job_type'], + buckets: [0.1, 0.5, 1, 2, 5, 10, 30], + registers: [this.registry], + }); + + // Email Campaigns Sent Counter + this.emailCampaignsSent = new Counter({ + name: 'email_campaigns_sent_total', + help: 'Total number of email campaigns sent', + labelNames: ['campaign_type', 'status'], + registers: [this.registry], + }); + + // Backup Operations Counter + this.backupOperations = new Counter({ + name: 'backup_operations_total', + help: 'Total number of backup operations', + labelNames: ['operation_type', 'status'], + registers: [this.registry], + }); } onModuleInit() { @@ -57,4 +121,33 @@ export class MetricsCollectionService implements OnModuleInit { recordDbQuery(queryType: string, table: string, duration: number) { this.dbQueryDuration.observe({ query_type: queryType, table }, duration); } + + // Custom business metrics methods + recordUserRegistration(userType: string, source: string) { + this.userRegistrations.inc({ user_type: userType, source }); + } + + recordAssessmentCompletion(assessmentType: string, difficulty: string) { + this.assessmentCompletions.inc({ assessment_type: assessmentType, difficulty }); + } + + updateLearningPathProgress(pathId: string, userId: string, progress: number) { + this.learningPathProgress.set({ path_id: pathId, user_id: userId }, progress); + } + + updateCacheHitRate(cacheType: string, hitRate: number) { + this.cacheHitRate.set({ cache_type: cacheType }, hitRate); + } + + recordQueueProcessingTime(queueName: string, jobType: string, duration: number) { + this.queueProcessingTime.observe({ queue_name: queueName, job_type: jobType }, duration); + } + + recordEmailCampaignSent(campaignType: string, status: string) { + this.emailCampaignsSent.inc({ campaign_type: campaignType, status }); + } + + recordBackupOperation(operationType: string, status: string) { + this.backupOperations.inc({ operation_type: operationType, status }); + } } diff --git a/src/monitoring/monitoring.controller.ts b/src/monitoring/monitoring.controller.ts index 80f0c5c8..1f6bfe4d 100644 --- a/src/monitoring/monitoring.controller.ts +++ b/src/monitoring/monitoring.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Res } from '@nestjs/common'; +import { Controller, Get, Res, Query } from '@nestjs/common'; import { MetricsCollectionService } from './metrics/metrics-collection.service'; import { Response } from 'express'; import { ScheduledTaskMonitoringService } from './scheduled-task-monitoring.service'; @@ -8,7 +8,7 @@ export class MonitoringController { constructor( private readonly metricsService: MetricsCollectionService, private readonly scheduledTaskMonitoringService: ScheduledTaskMonitoringService, - ) {} + ) { } @Get() async getMetrics(@Res() res: Response) { @@ -17,6 +17,105 @@ export class MonitoringController { res.send(metrics); } + @Get('unified') + async getUnifiedMetrics( + @Query('format') format?: string, + @Query('include') include?: string, + @Query('exclude') exclude?: string, + ) { + const includeTypes = include?.split(',').map(s => s.trim()) || []; + const excludeTypes = exclude?.split(',').map(s => s.trim()) || []; + + // Get base Prometheus metrics + const prometheusMetrics = await this.metricsService.getMetrics(); + + // Get scheduled tasks dashboard + const scheduledTasksMetrics = this.scheduledTaskMonitoringService.getDashboard(); + + // Aggregate metrics from different sources + const unifiedMetrics = { + prometheus: prometheusMetrics, + scheduledTasks: scheduledTasksMetrics, + timestamp: new Date().toISOString(), + metadata: { + totalMetrics: prometheusMetrics.split('\n').filter(line => line && !line.startsWith('#')).length, + includeTypes, + excludeTypes, + } + }; + + // Return in requested format + if (format === 'json') { + return unifiedMetrics; + } + + // Default to Prometheus format + const metrics = await this.metricsService.getMetrics(); + return metrics; + } + + @Get('health') + async getMetricsHealth() { + return { + status: 'healthy', + timestamp: new Date().toISOString(), + services: { + metricsCollection: 'active', + scheduledTasks: 'active', + }, + registry: { + metricsCount: (await this.metricsService.getMetrics()).split('\n').filter(line => line && !line.startsWith('#')).length, + } + }; + } + + @Get('custom') + async getCustomMetrics(@Query('type') type?: string) { + const customMetrics = { + user_registrations: { + name: 'user_registrations_total', + help: 'Total number of user registrations', + type: 'counter', + }, + assessment_completions: { + name: 'assessment_completions_total', + help: 'Total number of assessment completions', + type: 'counter', + }, + learning_path_progress: { + name: 'learning_path_progress_percentage', + help: 'Average learning path progress percentage', + type: 'gauge', + }, + cache_hit_rate: { + name: 'cache_hit_rate_percentage', + help: 'Cache hit rate percentage', + type: 'gauge', + }, + queue_processing_time: { + name: 'queue_processing_duration_seconds', + help: 'Duration of queue job processing in seconds', + type: 'histogram', + }, + email_campaigns_sent: { + name: 'email_campaigns_sent_total', + help: 'Total number of email campaigns sent', + type: 'counter', + }, + backup_operations: { + name: 'backup_operations_total', + help: 'Total number of backup operations', + type: 'counter', + }, + }; + + if (type) { + return customMetrics[type] || { error: 'Metric type not found' }; + } + + return customMetrics; + } + @Get('scheduled-tasks/dashboard') getScheduledTasksDashboard() { return this.scheduledTaskMonitoringService.getDashboard();