UNPKG

@shopify/cli-kit

Version:

A set of utilities, interfaces, and models that are common across all the platform features

112 lines 4.67 kB
import { ExplicitBucketHistogramAggregation, View } from '@opentelemetry/sdk-metrics'; import { MetricInstrumentType } from '../types.js'; import { isValidMetricName } from '../../utils/validators.js'; const instrumentationScope = 'opentelemetry-js-shopify-web'; export class BaseOtelService { /** * Bootstraps an Otel exporter which can send Otel metrics to a dedicated Shopify supported collector endpoint. */ constructor({ serviceName, prefixMetric = false, metrics = {}, onRecord, meterProvider }) { this.metrics = new Map(); this.recordListeners = new Set(); if (!serviceName) { throw new Error('Service name is required.'); } this.serviceName = serviceName; this.prefixMetric = prefixMetric; if (onRecord) this.addOnRecord(onRecord); if (!meterProvider) { throw new Error('MeterProvider is required.'); } this.meterProvider = meterProvider; this.register(metrics); } getMeterProvider() { return this.meterProvider; } addView(viewOptions) { // The API to register view is not yet exposed. We need to use the private // property to register a new view after the initial instantiation. ; this.meterProvider._sharedState?.viewRegistry?.addView?.(new View(viewOptions)); } record(metricName, value, labels) { const recordMetric = this.metrics.get(metricName); if (!recordMetric) { throw new Error(`Service ${this.serviceName} has no metrics registered for name: ${metricName}. Can't record value for unknown metric.`); } recordMetric(value, labels); } registerMetric(metricName, { type, ...options }) { if (this.metrics.has(metricName)) { return; } const meter = this.meterProvider.getMeter(instrumentationScope); const name = this.prefixMetric ? `${this.serviceName}_${metricName}` : metricName; if (!isValidMetricName(name)) { return; } const createInstrument = () => { switch (type) { case MetricInstrumentType.Counter: return meter.createCounter(name, options); case MetricInstrumentType.UpDownCounter: return meter.createUpDownCounter(name, options); case MetricInstrumentType.Histogram: { if ('boundaries' in options) { this.addView({ instrumentName: name, aggregation: new ExplicitBucketHistogramAggregation(options.boundaries, true), }); } return meter.createHistogram(name, options); } } }; // Lazy instantiate the instrument so we don't create it if we don't need to this.metrics.set(metricName, (firstValue, firstLabels) => { const instrument = createInstrument(); const record = (value, labels) => { const [finalValue, finalLabels] = this.notifyRecordListeners(metricName, value, // ensures an new object is created so we don't mutate the original { ...labels }); if ('record' in instrument) { instrument.record(finalValue, finalLabels); } else { instrument.add(finalValue, finalLabels); } // We flush metrics after every record - we do not await as we fire & forget. this.meterProvider.forceFlush({}); }; record(firstValue, firstLabels); this.metrics.set(metricName, record); }); } register(metrics) { Object.entries(metrics).forEach(([metricName, options]) => { this.registerMetric(metricName, options); }); } addOnRecord(onRecord) { this.recordListeners.add(onRecord); return () => { this.recordListeners.delete(onRecord); }; } removeOnRecord(onRecord) { this.recordListeners.delete(onRecord); } shutdown() { this.metrics.clear(); this.recordListeners.clear(); return this.meterProvider.shutdown(); } notifyRecordListeners(metricName, initialValue, initialLabels) { return Array.from(this.recordListeners).reduce((recordArgs, listener) => { return listener(metricName, ...recordArgs) || recordArgs; }, [initialValue, initialLabels]); } } //# sourceMappingURL=BaseOtelService.js.map