feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View 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() });
});
}

View 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;
}

View 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';

View File

@@ -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 };
}

View File

@@ -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 });
});
});