Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@nestjs/typeorm": "^11.0.1",
"@types/cookie-parser": "^1.4.8",
"@types/multer": "^2.0.0",
"@willsoto/nestjs-prometheus": "^6.1.0",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.3",
"cache-manager": "^7.2.8",
Expand All @@ -70,6 +71,7 @@
"passport-custom": "^1.1.1",
"passport-jwt": "^4.0.1",
"pg": "^8.18.0",
"prom-client": "^15.1.3",
"redis": "^5.12.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
Expand Down
29 changes: 29 additions & 0 deletions src/modules/metrics/basic-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class BasicAuthGuard implements CanActivate {
constructor(private configService: ConfigService) {}

canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;

if (!authHeader || !authHeader.startsWith('Basic ')) {
throw new UnauthorizedException('Missing or invalid authorization header');
}

const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
const [username, password] = credentials.split(':');

const expectedUsername = this.configService.get<string>('METRICS_USERNAME', 'admin');
const expectedPassword = this.configService.get<string>('METRICS_PASSWORD', 'password');

if (username !== expectedUsername || password !== expectedPassword) {
throw new UnauthorizedException('Invalid credentials');
}

return true;
}
}
36 changes: 36 additions & 0 deletions src/modules/metrics/metrics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { PrometheusController } from '@willsoto/nestjs-prometheus';
import { MetricsService } from './metrics.service';
import { BasicAuthGuard } from './basic-auth.guard';

@ApiTags('Metrics')
@Controller()
export class MetricsController extends PrometheusController {
constructor(private readonly metricsService: MetricsService) {
super();
}

@Get('/metrics')
@UseGuards(BasicAuthGuard)
@ApiOperation({
summary: 'Get Prometheus metrics',
description: 'Returns Prometheus-compatible metrics for monitoring and alerting.',
})
@ApiResponse({
status: 200,
description: 'Prometheus metrics in text format',
content: {
'text/plain': {
example: '# HELP http_requests_total Total number of HTTP requests\n# TYPE http_requests_total counter\nhttp_requests_total{method="GET",status="200"} 42\n',
},
},
})
@ApiResponse({
status: 401,
description: 'Unauthorized - Invalid credentials',
})
async getMetrics() {
return super.index();
}
}
22 changes: 22 additions & 0 deletions src/modules/metrics/metrics.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
import { MetricsController } from './metrics.controller';
import { MetricsService } from './metrics.service';

@Module({
imports: [
PrometheusModule.register({
defaultMetrics: {
enabled: true,
},
path: '/metrics',
defaultLabels: {
app: 'skillsync_server',
},
}),
],
controllers: [MetricsController],
providers: [MetricsService],
exports: [MetricsService],
})
export class MetricsModule {}
78 changes: 78 additions & 0 deletions src/modules/metrics/metrics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Injectable, Inject } from '@nestjs/common';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Counter, Histogram, Gauge } from 'prom-client';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
// Assuming you have User entity, adjust as needed
import { User } from '../user/user.entity';

@Injectable()
export class MetricsService {
constructor(
@InjectMetric('http_requests_total')
private readonly httpRequestsTotal: Counter<string>,
@InjectMetric('http_request_duration_seconds')
private readonly httpRequestDuration: Histogram<string>,
@InjectMetric('db_query_duration_seconds')
private readonly dbQueryDuration: Histogram<string>,
@InjectMetric('db_connection_pool_size')
private readonly dbConnectionPoolSize: Gauge<string>,
@InjectMetric('redis_operation_duration_seconds')
private readonly redisOperationDuration: Histogram<string>,
@InjectMetric('active_users')
private readonly activeUsers: Gauge<string>,
@InjectMetric('jwt_verification_failures_total')
private readonly jwtVerificationFailures: Counter<string>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}

// HTTP metrics
incrementHttpRequests(method: string, status: string, endpoint: string) {
this.httpRequestsTotal
.labels({ method, status, endpoint })
.inc();
}

recordHttpRequestDuration(method: string, status: string, endpoint: string, duration: number) {
this.httpRequestDuration
.labels({ method, status, endpoint })
.observe(duration);
}

// Database metrics
recordDbQueryDuration(query: string, duration: number) {
this.dbQueryDuration
.labels({ query })
.observe(duration);
}

setDbConnectionPoolSize(size: number) {
this.dbConnectionPoolSize.set(size);
}

// Redis metrics
recordRedisOperationDuration(operation: string, duration: number) {
this.redisOperationDuration
.labels({ operation })
.observe(duration);
}

// Application metrics
async updateActiveUsers() {
// Assuming active users are those who logged in within the last 24 hours
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const activeUserCount = await this.userRepository.count({
where: {
lastLoginAt: { $gte: twentyFourHoursAgo } as any, // Adjust based on your User entity
},
});
this.activeUsers.set(activeUserCount);
}

incrementJwtVerificationFailures(reason: string) {
this.jwtVerificationFailures
.labels({ reason })
.inc();
}
}