feat: bootstrap from Fabric monorepo
This commit is contained in:
34
src/common/api-key.guard.ts
Normal file
34
src/common/api-key.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
ServiceUnavailableException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest<{ path?: string; headers: Record<string, string | string[] | undefined> }>();
|
||||
const path = req.path ?? '';
|
||||
|
||||
// allow health check without auth
|
||||
if (path.endsWith('/healthz') || path === '/healthz') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const expected = process.env.FABRIC_API_KEY;
|
||||
if (!expected || expected.trim() === '') {
|
||||
throw new ServiceUnavailableException('FABRIC_API_KEY is not configured');
|
||||
}
|
||||
|
||||
const received = req.headers['x-api-key'];
|
||||
const receivedValue = Array.isArray(received) ? received[0] : received;
|
||||
|
||||
if (!receivedValue || receivedValue !== expected) {
|
||||
throw new UnauthorizedException('invalid api key');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
9
src/common/health.controller.ts
Normal file
9
src/common/health.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('healthz')
|
||||
export class HealthController {
|
||||
@Get()
|
||||
get() {
|
||||
return { ok: true, service: 'guild' };
|
||||
}
|
||||
}
|
||||
12
src/common/metrics.controller.ts
Normal file
12
src/common/metrics.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { MetricsService } from './metrics.service';
|
||||
|
||||
@Controller('metrics')
|
||||
export class MetricsController {
|
||||
constructor(private readonly metrics: MetricsService) {}
|
||||
|
||||
@Get()
|
||||
get() {
|
||||
return this.metrics.snapshot();
|
||||
}
|
||||
}
|
||||
35
src/common/metrics.service.ts
Normal file
35
src/common/metrics.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
type Bucket = {
|
||||
requests: number;
|
||||
errors: number;
|
||||
totalDurationMs: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class MetricsService {
|
||||
private readonly bucket: Bucket = { requests: 0, errors: 0, totalDurationMs: 0 };
|
||||
private startedAt = Date.now();
|
||||
|
||||
record(statusCode: number, durationMs: number): void {
|
||||
this.bucket.requests += 1;
|
||||
if (statusCode >= 400) this.bucket.errors += 1;
|
||||
this.bucket.totalDurationMs += durationMs;
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
const uptimeSec = Math.max(1, Math.floor((Date.now() - this.startedAt) / 1000));
|
||||
const qps = this.bucket.requests / uptimeSec;
|
||||
const avgLatencyMs = this.bucket.requests > 0 ? this.bucket.totalDurationMs / this.bucket.requests : 0;
|
||||
const errorRate = this.bucket.requests > 0 ? this.bucket.errors / this.bucket.requests : 0;
|
||||
|
||||
return {
|
||||
requests: this.bucket.requests,
|
||||
errors: this.bucket.errors,
|
||||
qps,
|
||||
avgLatencyMs,
|
||||
errorRate,
|
||||
uptimeSec,
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/common/request-context.middleware.ts
Normal file
36
src/common/request-context.middleware.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { MetricsService } from './metrics.service';
|
||||
|
||||
type ReqWithId = Request & { requestId?: string };
|
||||
|
||||
export function createRequestContextMiddleware(service: 'center' | 'guild', metrics: MetricsService) {
|
||||
return (req: ReqWithId, res: Response, next: NextFunction): void => {
|
||||
const headerId = req.headers['x-request-id'];
|
||||
const requestId =
|
||||
(Array.isArray(headerId) ? headerId[0] : headerId) || randomUUID();
|
||||
|
||||
req.requestId = requestId;
|
||||
res.setHeader('x-request-id', requestId);
|
||||
|
||||
const startedAt = Date.now();
|
||||
res.on('finish', () => {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
metrics.record(res.statusCode, durationMs);
|
||||
|
||||
const log = {
|
||||
level: 'info',
|
||||
service,
|
||||
requestId,
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
statusCode: res.statusCode,
|
||||
durationMs,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
console.log(JSON.stringify(log));
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user