import { Counter, Histogram, Meter, MeterOptions, MetricOptions, Attributes as OtelAttributes, } from '@opentelemetry/api'; import opentelemetry, { meterProvider } from './opentelemetry'; type CounterWrapper = ( attributes?: Attributes, increment?: number, ) => void; type HistogramWrapper = ( value: number, attributes?: Attributes, ) => void; const DEFAULT_METER_NAME = process.env.OTEL_SERVICE_NAME || process.env.TELEMETRY_SERVICE_NAME || 'service'; /** * @see https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels */ const METRIC_REGEXP = new RegExp( `^${process.env.TELEMETRY_METRIC_REGEXP || '[a-zA-Z_:][a-zA-Z0-9_:]*'}$`, ); const meters = new Map(); const counters = new Map< string, { getCounter: () => Counter; wrapper: CounterWrapper } >(); const histograms = new Map< string, { getHistogram: () => Histogram; wrapper: HistogramWrapper } >(); function createMeter( name: string, version?: string, options?: MeterOptions, ): Meter { if (meters.has(name)) { return meters.get(name)!; } const meter = meterProvider().getMeter(name, version, options); meters.set(name, meter); return meter; } function assertValidMetricName(metric: string): void | never { if (!METRIC_REGEXP.test(metric)) { throw new Error('Invalid metric name format'); } } export function createCounter( name: string, description: string, unit = 'total', meterName = DEFAULT_METER_NAME, ): CounterWrapper { assertValidMetricName(name); if (counters.has(name)) { return counters.get(name)!.wrapper; } const meter = meters.get(meterName) ?? createMeter(DEFAULT_METER_NAME); let counter: Counter; const getCounter = () => { if (counter) { return counter; } counter = meter.createCounter(name, { description, unit, }); return counter; }; const wrapper: CounterWrapper = (attributes, increment) => { getCounter().add(increment ?? 1, attributes); }; counters.set(name, { getCounter, wrapper }); return wrapper; } export function createHistogram( name: string, description: string, buckets: number[], unit: string | undefined = 'total', meterName = DEFAULT_METER_NAME, ): HistogramWrapper { assertValidMetricName(name); if (histograms.has(name)) { return histograms.get(name)!.wrapper; } const meter = meters.get(meterName) ?? createMeter(DEFAULT_METER_NAME); let histogram: Histogram; const getHistogram = () => { if (histogram) { return histogram; } histogram = meter.createHistogram(name, { description, unit, advice: { explicitBucketBoundaries: buckets, }, }); return histogram; }; const wrapper: HistogramWrapper = (value, attributes) => { getHistogram().record(value, attributes); }; histograms.set(name, { getHistogram, wrapper }); return wrapper; } export function metric( metric: string, context: Record = {}, options: opentelemetry.api.MetricOptions = {}, increment = 1, meterName = DEFAULT_METER_NAME, ): void { void createCounter( metric, options?.description ?? 'Description missing', options?.unit, meterName, )(context, increment); }