/** * Label map for metrics */ export type Labels = Record; /** * Metric types */ export enum MetricType { COUNTER = 'counter', GAUGE = 'gauge', HISTOGRAM = 'histogram', } /** * Histogram bucket configuration */ export interface HistogramBuckets { buckets: number[]; counts: Map; sum: number; count: number; } /** * Base metric class */ abstract class Metric { constructor( public readonly name: string, public readonly type: MetricType, public readonly help: string, public readonly labels: string[] = [] ) {} abstract getValue(labels?: Labels): number | HistogramBuckets; abstract reset(): void; } /** * Counter metric - monotonically increasing value * * @example * ```typescript * const requestCounter = new Counter('http_requests_total', 'Total HTTP requests', ['method', 'status']); * requestCounter.inc({ method: 'GET', status: '200' }); * requestCounter.inc({ method: 'POST', status: '201' }, 5); * ``` */ export class Counter extends Metric { private values: Map = new Map(); constructor(name: string, help: string, labels: string[] = []) { super(name, MetricType.COUNTER, help, labels); } /** * Increment counter */ inc(labels: Labels = {}, value: number = 1): void { if (value < 0) { throw new Error('Counter can only be incremented with positive values'); } const key = this.getKey(labels); const current = this.values.get(key) || 0; this.values.set(key, current + value); } /** * Get current value */ getValue(labels: Labels = {}): number { const key = this.getKey(labels); return this.values.get(key) || 0; } /** * Reset counter */ reset(): void { this.values.clear(); } /** * Get all values with labels */ getAll(): Array<{ labels: Labels; value: number }> { const results: Array<{ labels: Labels; value: number }> = []; for (const [key, value] of this.values.entries()) { const labels = this.parseKey(key); results.push({ labels, value }); } return results; } private getKey(labels: Labels): string { const labelStr = this.labels .map((label) => `${label}=${labels[label] || ''}`) .join(','); return labelStr || 'default'; } private parseKey(key: string): Labels { if (key === 'default') return {}; const labels: Labels = {}; const pairs = key.split(','); for (const pair of pairs) { const [name, value] = pair.split('='); if (name && value !== undefined) { labels[name] = value; } } return labels; } } /** * Gauge metric - value that can go up and down * * @example * ```typescript * const activeConnections = new Gauge('active_connections', 'Number of active connections'); * activeConnections.set(42); * activeConnections.inc(); * activeConnections.dec(5); * ``` */ export class Gauge extends Metric { private values: Map = new Map(); constructor(name: string, help: string, labels: string[] = []) { super(name, MetricType.GAUGE, help, labels); } /** * Set gauge to a specific value */ set(value: number, labels: Labels = {}): void { const key = this.getKey(labels); this.values.set(key, value); } /** * Increment gauge */ inc(labels: Labels = {}, value: number = 1): void { const key = this.getKey(labels); const current = this.values.get(key) || 0; this.values.set(key, current + value); } /** * Decrement gauge */ dec(labels: Labels = {}, value: number = 1): void { this.inc(labels, -value); } /** * Get current value */ getValue(labels: Labels = {}): number { const key = this.getKey(labels); return this.values.get(key) || 0; } /** * Reset gauge */ reset(): void { this.values.clear(); } /** * Get all values with labels */ getAll(): Array<{ labels: Labels; value: number }> { const results: Array<{ labels: Labels; value: number }> = []; for (const [key, value] of this.values.entries()) { const labels = this.parseKey(key); results.push({ labels, value }); } return results; } private getKey(labels: Labels): string { const labelStr = this.labels .map((label) => `${label}=${labels[label] || ''}`) .join(','); return labelStr || 'default'; } private parseKey(key: string): Labels { if (key === 'default') return {}; const labels: Labels = {}; const pairs = key.split(','); for (const pair of pairs) { const [name, value] = pair.split('='); if (name && value !== undefined) { labels[name] = value; } } return labels; } } /** * Histogram metric - tracks distribution of values * * @example * ```typescript * const latency = new Histogram('request_duration_seconds', 'Request latency', ['endpoint'], [0.1, 0.5, 1, 2, 5]); * latency.observe(0.234, { endpoint: '/api/users' }); * latency.observe(1.567, { endpoint: '/api/users' }); * ``` */ export class Histogram extends Metric { private buckets: number[]; private values: Map = new Map(); constructor( name: string, help: string, labels: string[] = [], buckets: number[] = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] ) { super(name, MetricType.HISTOGRAM, help, labels); this.buckets = [...buckets].sort((a, b) => a - b); } /** * Observe a value */ observe(value: number, labels: Labels = {}): void { const key = this.getKey(labels); let bucketData = this.values.get(key); if (!bucketData) { bucketData = { buckets: this.buckets, counts: new Map(this.buckets.map((b) => [b, 0])), sum: 0, count: 0, }; this.values.set(key, bucketData); } // Update bucket counts for (const bucket of this.buckets) { if (value <= bucket) { const current = bucketData.counts.get(bucket) || 0; bucketData.counts.set(bucket, current + 1); } } bucketData.sum += value; bucketData.count++; } /** * Get histogram data */ getValue(labels: Labels = {}): HistogramBuckets { const key = this.getKey(labels); return ( this.values.get(key) || { buckets: this.buckets, counts: new Map(this.buckets.map((b) => [b, 0])), sum: 0, count: 0, } ); } /** * Reset histogram */ reset(): void { this.values.clear(); } /** * Get all histogram data with labels */ getAll(): Array<{ labels: Labels; value: HistogramBuckets }> { const results: Array<{ labels: Labels; value: HistogramBuckets }> = []; for (const [key, value] of this.values.entries()) { const labels = this.parseKey(key); results.push({ labels, value }); } return results; } private getKey(labels: Labels): string { const labelStr = this.labels .map((label) => `${label}=${labels[label] || ''}`) .join(','); return labelStr || 'default'; } private parseKey(key: string): Labels { if (key === 'default') return {}; const labels: Labels = {}; const pairs = key.split(','); for (const pair of pairs) { const [name, value] = pair.split('='); if (name && value !== undefined) { labels[name] = value; } } return labels; } } /** * Metrics registry */ export class MetricsRegistry { private metrics: Map = new Map(); /** * Register a metric */ register(metric: Metric): void { if (this.metrics.has(metric.name)) { throw new Error(`Metric ${metric.name} already registered`); } this.metrics.set(metric.name, metric); } /** * Get a metric by name */ get(name: string): Metric | undefined { return this.metrics.get(name); } /** * Get all metrics */ getAll(): Metric[] { return Array.from(this.metrics.values()); } /** * Clear all metrics */ clear(): void { this.metrics.clear(); } /** * Reset all metric values */ reset(): void { for (const metric of this.metrics.values()) { metric.reset(); } } /** * Export metrics in Prometheus text format */ export(): string { const lines: string[] = []; for (const metric of this.metrics.values()) { // Add help text lines.push(`# HELP ${metric.name} ${metric.help}`); lines.push(`# TYPE ${metric.name} ${metric.type}`); if (metric instanceof Counter || metric instanceof Gauge) { const all = metric.getAll(); for (const { labels, value } of all) { const labelStr = Object.entries(labels) .map(([k, v]) => `${k}="${v}"`) .join(','); const metricLine = labelStr ? `${metric.name}{${labelStr}} ${value}` : `${metric.name} ${value}`; lines.push(metricLine); } } else if (metric instanceof Histogram) { const all = metric.getAll(); for (const { labels, value } of all) { const labelStr = Object.entries(labels) .map(([k, v]) => `${k}="${v}"`) .join(','); const labelPrefix = labelStr ? `{${labelStr}}` : ''; // Export bucket counts for (const [bucket, count] of value.counts.entries()) { const bucketLabel = labelStr ? `{${labelStr},le="${bucket}"}` : `{le="${bucket}"}`; lines.push(`${metric.name}_bucket${bucketLabel} ${count}`); } // Export +Inf bucket const infLabel = labelStr ? `{${labelStr},le="+Inf"}` : `{le="+Inf"}`; lines.push(`${metric.name}_bucket${infLabel} ${value.count}`); // Export sum and count lines.push(`${metric.name}_sum${labelPrefix} ${value.sum}`); lines.push(`${metric.name}_count${labelPrefix} ${value.count}`); } } lines.push(''); // Empty line between metrics } return lines.join('\n'); } } /** * Default metrics registry */ export const defaultRegistry = new MetricsRegistry(); /** * Metrics collector for Elasticsearch client */ export class MetricsCollector { public readonly registry: MetricsRegistry; // Standard metrics public readonly requestsTotal: Counter; public readonly requestDuration: Histogram; public readonly requestErrors: Counter; public readonly activeConnections: Gauge; public readonly bulkOperations: Counter; public readonly bulkDocuments: Counter; public readonly retries: Counter; constructor(registry: MetricsRegistry = defaultRegistry) { this.registry = registry; // Initialize standard metrics this.requestsTotal = new Counter( 'elasticsearch_requests_total', 'Total number of Elasticsearch requests', ['operation', 'index'] ); this.registry.register(this.requestsTotal); this.requestDuration = new Histogram( 'elasticsearch_request_duration_seconds', 'Elasticsearch request duration in seconds', ['operation', 'index'] ); this.registry.register(this.requestDuration); this.requestErrors = new Counter( 'elasticsearch_request_errors_total', 'Total number of Elasticsearch request errors', ['operation', 'index', 'error_code'] ); this.registry.register(this.requestErrors); this.activeConnections = new Gauge( 'elasticsearch_active_connections', 'Number of active Elasticsearch connections' ); this.registry.register(this.activeConnections); this.bulkOperations = new Counter( 'elasticsearch_bulk_operations_total', 'Total number of bulk operations', ['index'] ); this.registry.register(this.bulkOperations); this.bulkDocuments = new Counter( 'elasticsearch_bulk_documents_total', 'Total number of documents in bulk operations', ['index', 'status'] ); this.registry.register(this.bulkDocuments); this.retries = new Counter( 'elasticsearch_retries_total', 'Total number of retry attempts', ['operation'] ); this.registry.register(this.retries); } /** * Create a custom counter */ counter(name: string, help: string, labels?: string[]): Counter { const counter = new Counter(name, help, labels); this.registry.register(counter); return counter; } /** * Create a custom gauge */ gauge(name: string, help: string, labels?: string[]): Gauge { const gauge = new Gauge(name, help, labels); this.registry.register(gauge); return gauge; } /** * Create a custom histogram */ histogram(name: string, help: string, labels?: string[], buckets?: number[]): Histogram { const histogram = new Histogram(name, help, labels, buckets); this.registry.register(histogram); return histogram; } /** * Record a counter increment (convenience method for plugins) */ recordCounter(name: string, value: number = 1, labels: Labels = {}): void { let counter = this.registry.get(name) as Counter | undefined; if (!counter) { counter = new Counter(name, `Counter: ${name}`, Object.keys(labels)); this.registry.register(counter); } counter.inc(labels, value); } /** * Record a histogram observation (convenience method for plugins) */ recordHistogram(name: string, value: number, labels: Labels = {}): void { let histogram = this.registry.get(name) as Histogram | undefined; if (!histogram) { histogram = new Histogram(name, `Histogram: ${name}`, Object.keys(labels)); this.registry.register(histogram); } histogram.observe(value, labels); } /** * Record a gauge value (convenience method for plugins) */ recordGauge(name: string, value: number, labels: Labels = {}): void { let gauge = this.registry.get(name) as Gauge | undefined; if (!gauge) { gauge = new Gauge(name, `Gauge: ${name}`, Object.keys(labels)); this.registry.register(gauge); } gauge.set(value, labels); } /** * Export all metrics in Prometheus format */ export(): string { return this.registry.export(); } } /** * Default metrics collector */ export const defaultMetricsCollector = new MetricsCollector();