feat(ADM-018): completed feature
This commit is contained in:
20
project/src/modules/observability/api/metrics.routes.ts
Normal file
20
project/src/modules/observability/api/metrics.routes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Authenticate } from '../../../shared/auth.js';
|
||||
import { requireRole } from '../../../shared/auth.js';
|
||||
import type { InMemoryMeter } from '../infrastructure/no-op-telemetry.js';
|
||||
|
||||
export interface MetricsRoutesDeps {
|
||||
meter: InMemoryMeter;
|
||||
authenticate: Authenticate;
|
||||
}
|
||||
|
||||
export async function registerMetricsRoutes(
|
||||
app: FastifyInstance,
|
||||
deps: MetricsRoutesDeps,
|
||||
): Promise<void> {
|
||||
app.get('/ops/metrics', async (request, reply) => {
|
||||
const user = await deps.authenticate(request);
|
||||
requireRole(user, 'admin');
|
||||
return reply.send({ metrics: deps.meter.snapshot() });
|
||||
});
|
||||
}
|
||||
29
project/src/modules/observability/domain/telemetry.ts
Normal file
29
project/src/modules/observability/domain/telemetry.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/** Minimal interfaces so observability is swappable without touching callers. */
|
||||
|
||||
export interface Span {
|
||||
end(): void;
|
||||
setError(err: unknown): void;
|
||||
}
|
||||
|
||||
export interface Tracer {
|
||||
startSpan(name: string): Span;
|
||||
trace<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
export interface Counter {
|
||||
add(value: number, attributes?: Record<string, string>): void;
|
||||
}
|
||||
|
||||
export interface Histogram {
|
||||
record(value: number, attributes?: Record<string, string>): void;
|
||||
}
|
||||
|
||||
export interface Meter {
|
||||
counter(name: string): Counter;
|
||||
histogram(name: string): Histogram;
|
||||
}
|
||||
|
||||
export interface Telemetry {
|
||||
tracer: Tracer;
|
||||
meter: Meter;
|
||||
}
|
||||
8
project/src/modules/observability/index.ts
Normal file
8
project/src/modules/observability/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/** Public API of the observability module. */
|
||||
export { registerMetricsRoutes, type MetricsRoutesDeps } from './api/metrics.routes.js';
|
||||
export {
|
||||
InMemoryMeter,
|
||||
createInMemoryTelemetry,
|
||||
createNoOpTelemetry,
|
||||
} from './infrastructure/no-op-telemetry.js';
|
||||
export type { Telemetry, Tracer, Meter, Span, Counter, Histogram } from './domain/telemetry.js';
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Histogram, Counter, Meter, Span, Tracer, Telemetry } from '../domain/telemetry.js';
|
||||
|
||||
class NoopSpan implements Span {
|
||||
end(): void {}
|
||||
setError(_err: unknown): void {}
|
||||
}
|
||||
|
||||
export class NoopTracer implements Tracer {
|
||||
startSpan(_name: string): Span {
|
||||
return new NoopSpan();
|
||||
}
|
||||
async trace<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
||||
return fn();
|
||||
}
|
||||
}
|
||||
|
||||
class NoopCounter implements Counter {
|
||||
add(_value: number, _attributes?: Record<string, string>): void {}
|
||||
}
|
||||
|
||||
class NoopHistogram implements Histogram {
|
||||
record(_value: number, _attributes?: Record<string, string>): void {}
|
||||
}
|
||||
|
||||
class NoopMeter implements Meter {
|
||||
counter(_name: string): Counter {
|
||||
return new NoopCounter();
|
||||
}
|
||||
histogram(_name: string): Histogram {
|
||||
return new NoopHistogram();
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryMeter implements Meter {
|
||||
readonly data = new Map<string, number>();
|
||||
counter(name: string): Counter {
|
||||
return {
|
||||
add: (value, _attributes) => {
|
||||
this.data.set(name, (this.data.get(name) ?? 0) + value);
|
||||
},
|
||||
};
|
||||
}
|
||||
histogram(_name: string): Histogram {
|
||||
return { record: () => undefined };
|
||||
}
|
||||
snapshot(): Record<string, number> {
|
||||
return Object.fromEntries(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
export function createNoOpTelemetry(): Telemetry {
|
||||
return { tracer: new NoopTracer(), meter: new NoopMeter() };
|
||||
}
|
||||
|
||||
export function createInMemoryTelemetry(): { telemetry: Telemetry; meter: InMemoryMeter } {
|
||||
const meter = new InMemoryMeter();
|
||||
return { telemetry: { tracer: new NoopTracer(), meter }, meter };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { InMemoryMeter } from '../infrastructure/no-op-telemetry.js';
|
||||
|
||||
describe('InMemoryMeter', () => {
|
||||
it('accumulates counter values', () => {
|
||||
const meter = new InMemoryMeter();
|
||||
meter.counter('checkout.completed').add(1);
|
||||
meter.counter('checkout.completed').add(3);
|
||||
expect(meter.snapshot()).toEqual({ 'checkout.completed': 4 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user