/** * Prometheus metrics for extraction-service. * * Exposed via GET /metrics (auto-registered by fastify-metrics). * Custom counters/histograms for extraction-specific telemetry. */ // ── In-memory counters (fastify-metrics handles HTTP metrics) ──── // These are simple counters since prom-client may not be directly available. // They're exposed via the /extract/metrics endpoint. interface MetricBucket { labels: Record; value: number; } class Counter { private _buckets = new Map(); constructor(public readonly name: string) {} inc(labels: Record, amount = 1): void { const key = JSON.stringify(labels); const existing = this._buckets.get(key); if (existing) { existing.value += amount; } else { this._buckets.set(key, { labels, value: amount }); } } toJSON(): Array<{ labels: Record; value: number }> { return [...this._buckets.values()]; } } class Histogram { private _observations: Array<{ labels: Record; value: number }> = []; private _sum = 0; private _count = 0; constructor(public readonly name: string) {} observe(labels: Record, value: number): void { this._observations.push({ labels, value }); this._sum += value; this._count++; } toJSON(): { count: number; sum: number; avg: number } { return { count: this._count, sum: Math.round(this._sum * 100) / 100, avg: this._count > 0 ? Math.round((this._sum / this._count) * 100) / 100 : 0, }; } } // ── Exported metrics ───────────────────────────────────────────── export const extractionRequestsTotal = new Counter('extraction_requests_total'); export const extractionDurationSeconds = new Histogram('extraction_duration_seconds'); export const extractionEntitiesExtracted = new Histogram('extraction_entities_extracted'); export const extractionCacheHitTotal = new Counter('extraction_cache_hit_total'); /** * Record an extraction event with all metric dimensions. */ export function recordExtraction(params: { taskId?: string; modelId?: string; productId?: string; status: 'success' | 'error' | 'cache_hit'; durationMs?: number; entityCount?: number; }): void { const labels = { task_id: params.taskId || 'unknown', model_id: params.modelId || 'unknown', product_id: params.productId || 'unknown', status: params.status, }; extractionRequestsTotal.inc(labels); if (params.status === 'cache_hit') { extractionCacheHitTotal.inc(labels); } if (params.durationMs !== undefined) { extractionDurationSeconds.observe(labels, params.durationMs / 1000); } if (params.entityCount !== undefined) { extractionEntitiesExtracted.observe(labels, params.entityCount); } } /** * Get all metrics as a JSON summary. */ export function getMetricsSummary(): Record { return { extraction_requests_total: extractionRequestsTotal.toJSON(), extraction_duration_seconds: extractionDurationSeconds.toJSON(), extraction_entities_extracted: extractionEntitiesExtracted.toJSON(), extraction_cache_hit_total: extractionCacheHitTotal.toJSON(), }; }