/** * Application Metrics * Agent 4: Production Deployment * * Provides application metrics for Prometheus monitoring. * Tracks request counts, response times, and application health. */ import { env } from '../config'; interface MetricValue { value: number; labels: Record; timestamp: number; } interface HistogramBucket { le: number; count: number; } interface Histogram { buckets: HistogramBucket[]; sum: number; count: number; labels: Record; } /** * Simple in-memory metrics store * In production, replace with prom-client for full Prometheus compatibility */ class MetricsRegistry { private counters: Map = new Map(); private gauges: Map = new Map(); private histograms: Map = new Map(); private readonly defaultBuckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]; /** * Increment a counter metric */ incrementCounter(name: string, labels: Record = {}, value = 1): void { const key = this.getKey(name, labels); const existing = this.counters.get(key) || []; // Find existing entry with same labels or create new const labelKey = JSON.stringify(labels); const existingEntry = existing.find(e => JSON.stringify(e.labels) === labelKey); if (existingEntry) { existingEntry.value += value; existingEntry.timestamp = Date.now(); } else { existing.push({ value, labels, timestamp: Date.now() }); this.counters.set(key, existing); } } /** * Set a gauge metric value */ setGauge(name: string, value: number, labels: Record = {}): void { const key = this.getKey(name, labels); this.gauges.set(key, { value, labels, timestamp: Date.now() }); } /** * Increment a gauge metric */ incrementGauge(name: string, labels: Record = {}, value = 1): void { const key = this.getKey(name, labels); const existing = this.gauges.get(key); const newValue = (existing?.value || 0) + value; this.gauges.set(key, { value: newValue, labels, timestamp: Date.now() }); } /** * Decrement a gauge metric */ decrementGauge(name: string, labels: Record = {}, value = 1): void { this.incrementGauge(name, labels, -value); } /** * Observe a value in a histogram */ observeHistogram(name: string, value: number, labels: Record = {}): void { const key = this.getKey(name, labels); let histograms = this.histograms.get(key); if (!histograms) { histograms = []; this.histograms.set(key, histograms); } const labelKey = JSON.stringify(labels); let histogram = histograms.find(h => JSON.stringify(h.labels) === labelKey); if (!histogram) { histogram = { buckets: this.defaultBuckets.map(le => ({ le, count: 0 })), sum: 0, count: 0, labels, }; histograms.push(histogram); } // Update histogram histogram.sum += value; histogram.count += 1; for (const bucket of histogram.buckets) { if (value <= bucket.le) { bucket.count += 1; } } } /** * Get all metrics in Prometheus format */ toPrometheusFormat(): string { const lines: string[] = []; const prefix = 'lgc'; // Counters for (const [name, values] of this.counters.entries()) { const metricName = `${prefix}_${name.replace(/[^a-zA-Z0-9_]/g, '_')}_total`; lines.push(`# HELP ${metricName} Counter metric`); lines.push(`# TYPE ${metricName} counter`); for (const v of values) { const labelStr = this.formatLabels(v.labels); lines.push(`${metricName}${labelStr} ${v.value}`); } } // Gauges const gaugeGroups = new Map(); for (const [key, value] of this.gauges.entries()) { const name = key.split('|')[0]; if (!gaugeGroups.has(name)) { gaugeGroups.set(name, []); } gaugeGroups.get(name)!.push(value); } for (const [name, values] of gaugeGroups.entries()) { const metricName = `${prefix}_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`; lines.push(`# HELP ${metricName} Gauge metric`); lines.push(`# TYPE ${metricName} gauge`); for (const v of values) { const labelStr = this.formatLabels(v.labels); lines.push(`${metricName}${labelStr} ${v.value}`); } } // Histograms for (const [name, histograms] of this.histograms.entries()) { const metricName = `${prefix}_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`; lines.push(`# HELP ${metricName} Histogram metric`); lines.push(`# TYPE ${metricName} histogram`); for (const h of histograms) { const baseLabels = this.formatLabels(h.labels); for (const bucket of h.buckets) { const bucketLabel = h.labels ? `,le="${bucket.le}"` : `le="${bucket.le}"`; const labelStr = baseLabels ? baseLabels.slice(0, -1) + bucketLabel + '}' : `{${bucketLabel.slice(1)}}`; lines.push(`${metricName}_bucket${labelStr} ${bucket.count}`); } const infLabel = h.labels ? baseLabels.slice(0, -1) + `,le="+Inf"}` : `{le="+Inf"}`; lines.push(`${metricName}_bucket${infLabel} ${h.count}`); lines.push(`${metricName}_sum${baseLabels} ${h.sum}`); lines.push(`${metricName}_count${baseLabels} ${h.count}`); } } return lines.join('\n'); } /** * Get metrics as JSON */ toJSON(): Record { return { counters: Object.fromEntries(this.counters), gauges: Object.fromEntries(this.gauges), histograms: Object.fromEntries(this.histograms), timestamp: new Date().toISOString(), }; } /** * Reset all metrics */ reset(): void { this.counters.clear(); this.gauges.clear(); this.histograms.clear(); } private getKey(name: string, labels: Record): string { return name; } private formatLabels(labels: Record): string { const entries = Object.entries(labels); if (entries.length === 0) return ''; return '{' + entries.map(([k, v]) => `${k}="${v}"`).join(',') + '}'; } } // Create singleton instance export const metrics = new MetricsRegistry(); // Pre-defined metric helpers export const httpMetrics = { requestTotal(method: string, path: string, statusCode: number): void { metrics.incrementCounter('http_requests', { method, path, status: String(statusCode), }); }, requestDuration(method: string, path: string, durationMs: number): void { metrics.observeHistogram('http_request_duration_seconds', durationMs / 1000, { method, path, }); }, activeConnections(delta: number): void { metrics.incrementGauge('http_active_connections', {}, delta); }, }; export const appMetrics = { plantsRegistered(count = 1): void { metrics.incrementCounter('plants_registered', {}, count); }, transportEvents(eventType: string, count = 1): void { metrics.incrementCounter('transport_events', { type: eventType }, count); }, agentCycleCompleted(agentName: string, durationMs: number): void { metrics.observeHistogram('agent_cycle_duration_seconds', durationMs / 1000, { agent: agentName, }); metrics.incrementCounter('agent_cycles', { agent: agentName }); }, activeAgents(count: number): void { metrics.setGauge('active_agents', count); }, blockchainBlocks(count: number): void { metrics.setGauge('blockchain_blocks', count); }, databaseConnections(count: number): void { metrics.setGauge('database_connections', count); }, }; // Export types export type { MetricValue, Histogram, HistogramBucket };