@shopify/cli-kit
Version:
A set of utilities, interfaces, and models that are common across all the platform features
112 lines • 4.67 kB
JavaScript
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