diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 7f01f8dc211..86b959f769e 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. ### :boom: Breaking Change +* feat(api) Add delegating no-op meter provider [#4858](https://github.com/open-telemetry/opentelemetry-js/pull/4858) @hectorhdzg + * Proxy meters now upgrade previously created instruments and batch callbacks once an SDK registers, mirroring the behavior of the tracing and logging APIs. + ### :rocket: (Enhancement) * feat(api): improve isValidSpanId, isValidTraceId performance [#5714](https://github.com/open-telemetry/opentelemetry-js/pull/5714) @seemk diff --git a/api/src/api/metrics.ts b/api/src/api/metrics.ts index 186e7cce4b2..1e2f350d2bb 100644 --- a/api/src/api/metrics.ts +++ b/api/src/api/metrics.ts @@ -16,13 +16,13 @@ import { Meter, MeterOptions } from '../metrics/Meter'; import { MeterProvider } from '../metrics/MeterProvider'; -import { NOOP_METER_PROVIDER } from '../metrics/NoopMeterProvider'; import { getGlobal, registerGlobal, unregisterGlobal, } from '../internal/global-utils'; import { DiagAPI } from './diag'; +import { ProxyMeterProvider } from '../metrics/ProxyMeterProvider'; const API_NAME = 'metrics'; @@ -32,6 +32,8 @@ const API_NAME = 'metrics'; export class MetricsAPI { private static _instance?: MetricsAPI; + private _proxyMeterProvider = new ProxyMeterProvider(); + /** Empty private constructor prevents end users from constructing a new instance of the API */ private constructor() {} @@ -49,14 +51,22 @@ export class MetricsAPI { * Returns true if the meter provider was successfully registered, else false. */ public setGlobalMeterProvider(provider: MeterProvider): boolean { - return registerGlobal(API_NAME, provider, DiagAPI.instance()); + const success = registerGlobal( + API_NAME, + this._proxyMeterProvider, + DiagAPI.instance() + ); + if (success) { + this._proxyMeterProvider.setDelegate(provider); + } + return success; } /** * Returns the global meter provider. */ public getMeterProvider(): MeterProvider { - return getGlobal(API_NAME) || NOOP_METER_PROVIDER; + return getGlobal(API_NAME) || this._proxyMeterProvider; } /** @@ -73,5 +83,6 @@ export class MetricsAPI { /** Remove the global meter provider */ public disable(): void { unregisterGlobal(API_NAME, DiagAPI.instance()); + this._proxyMeterProvider = new ProxyMeterProvider(); } } diff --git a/api/src/index.ts b/api/src/index.ts index 0b4de61811d..c8ac1f8dda6 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -45,6 +45,7 @@ export type { export type { DiagAPI } from './api/diag'; // Metrics APIs +export { ProxyMeterProvider } from './metrics/ProxyMeterProvider'; export { createNoopMeter } from './metrics/NoopMeter'; export type { MeterOptions, Meter } from './metrics/Meter'; export type { MeterProvider } from './metrics/MeterProvider'; diff --git a/api/src/metrics/ProxyMeter.ts b/api/src/metrics/ProxyMeter.ts new file mode 100644 index 00000000000..9d60f96f2fc --- /dev/null +++ b/api/src/metrics/ProxyMeter.ts @@ -0,0 +1,459 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Meter, MeterOptions } from './Meter'; +import { + NOOP_COUNTER_METRIC, + NOOP_GAUGE_METRIC, + NOOP_HISTOGRAM_METRIC, + NOOP_METER, + NOOP_OBSERVABLE_COUNTER_METRIC, + NOOP_OBSERVABLE_GAUGE_METRIC, + NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC, + NOOP_UP_DOWN_COUNTER_METRIC, +} from './NoopMeter'; +import { + BatchObservableCallback, + Counter, + Gauge, + Histogram, + MetricOptions, + Observable, + ObservableCallback, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +} from './Metric'; + +const INTERNAL_NOOP_METER = NOOP_METER; + +/** + * Proxy meter provided by the proxy meter provider + */ +export class ProxyMeter implements Meter { + private _delegate?: Meter; + private readonly _instruments = new Set>(); + private readonly _batchCallbacks = new Map< + BatchObservableCallback, + Observable[] + >(); + + constructor( + private readonly _provider: MeterDelegator, + private readonly _name: string, + private readonly _version?: string, + private readonly _options?: MeterOptions + ) {} + + /** + * @see {@link Meter.createGauge} + */ + createGauge(name: string, options?: MetricOptions): Gauge { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createGauge(name, options); + } + + const instrument = new ProxyGauge(() => + this._delegate?.createGauge(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.createHistogram} + */ + createHistogram(name: string, options?: MetricOptions): Histogram { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createHistogram(name, options); + } + + const instrument = new ProxyHistogram(() => + this._delegate?.createHistogram(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.createCounter} + */ + createCounter(name: string, options?: MetricOptions): Counter { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createCounter(name, options); + } + + const instrument = new ProxyCounter(() => + this._delegate?.createCounter(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.createUpDownCounter} + */ + createUpDownCounter(name: string, options?: MetricOptions): UpDownCounter { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createUpDownCounter(name, options); + } + + const instrument = new ProxyUpDownCounter(() => + this._delegate?.createUpDownCounter(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.createObservableGauge} + */ + createObservableGauge( + name: string, + options?: MetricOptions + ): ObservableGauge { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createObservableGauge(name, options); + } + + const instrument = new ProxyObservableGauge(() => + this._delegate?.createObservableGauge(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.createObservableCounter} + */ + createObservableCounter( + name: string, + options?: MetricOptions + ): ObservableCounter { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createObservableCounter(name, options); + } + + const instrument = new ProxyObservableCounter(() => + this._delegate?.createObservableCounter(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.createObservableUpDownCounter} + */ + createObservableUpDownCounter( + name: string, + options?: MetricOptions + ): ObservableUpDownCounter { + const delegate = this._getDelegateOrUndefined(); + if (delegate) { + return delegate.createObservableUpDownCounter(name, options); + } + + const instrument = new ProxyObservableUpDownCounter(() => + this._delegate?.createObservableUpDownCounter(name, options) + ); + this._trackInstrument(instrument); + return instrument; + } + + /** + * @see {@link Meter.addBatchObservableCallback} + */ + addBatchObservableCallback( + callback: BatchObservableCallback, + observables: Observable[] + ): void { + const delegate = this._getDelegateOrUndefined(); + if (!delegate) { + this._batchCallbacks.set(callback, observables); + return; + } + + delegate.addBatchObservableCallback( + callback, + this._mapObservablesToDelegates(observables) + ); + } + + /** + * @see {@link Meter.removeBatchObservableCallback} + */ + removeBatchObservableCallback( + callback: BatchObservableCallback, + observables: Observable[] + ): void { + this._batchCallbacks.delete(callback); + this._getMeter().removeBatchObservableCallback(callback, observables); + } + + /** + * Ensure this proxy binds to the delegate meter if available. + */ + _bindDelegate(): void { + this._getDelegateOrUndefined(); + } + + private _trackInstrument(instrument: ProxyInstrumentBase) { + if (instrument.hasDelegate()) { + return; + } + this._instruments.add(instrument); + } + + private _mapObservablesToDelegates(observables: Observable[]): Observable[] { + return observables.map(observable => { + if (observable instanceof ProxyInstrumentBase) { + observable.bindDelegate(); + return (observable.getDelegateIfBound() ?? observable) as Observable; + } + + return observable; + }); + } + + private _flushPendingState() { + this._bindPendingInstruments(); + this._flushBatchObservableCallbacks(); + } + + private _bindPendingInstruments() { + if (!this._delegate) { + return; + } + + for (const instrument of this._instruments) { + instrument.bindDelegate(); + if (instrument.hasDelegate()) { + this._instruments.delete(instrument); + } + } + } + + private _flushBatchObservableCallbacks() { + if (!this._delegate || this._batchCallbacks.size === 0) { + return; + } + + for (const [callback, observables] of this._batchCallbacks) { + this._delegate.addBatchObservableCallback( + callback, + this._mapObservablesToDelegates(observables) + ); + } + this._batchCallbacks.clear(); + } + + /** + * Try to get a meter from the proxy meter provider. + * If the proxy meter provider has no delegate, return a noop meter. + */ + private _getMeter() { + return this._getDelegateOrUndefined() ?? INTERNAL_NOOP_METER; + } + + private _getDelegateOrUndefined(): Meter | undefined { + if (this._delegate) { + return this._delegate; + } + + const meter = this._provider.getDelegateMeter( + this._name, + this._version, + this._options + ); + + if (!meter) { + return undefined; + } + + this._delegate = meter; + this._provider._onProxyMeterDelegateBound(this); + this._flushPendingState(); + return this._delegate; + } +} + +abstract class ProxyInstrumentBase { + private _delegate?: T; + + constructor( + private readonly _delegateFactory: () => T | undefined, + private readonly _fallback: T + ) {} + + hasDelegate(): boolean { + return this._delegate != null; + } + + bindDelegate(): void { + if (this._delegate) { + return; + } + + const delegate = this._delegateFactory(); + if (!delegate) { + return; + } + + this._delegate = delegate; + this._onDelegateAttached(delegate); + } + + getDelegateIfBound(): T | undefined { + return this._delegate; + } + + protected _getDelegate(): T { + if (this._delegate) { + return this._delegate; + } + + const delegate = this._delegateFactory(); + if (!delegate) { + return this._fallback; + } + + this._delegate = delegate; + this._onDelegateAttached(delegate); + return delegate; + } + + protected abstract _onDelegateAttached(delegate: T): void; +} + +class ProxyCounter extends ProxyInstrumentBase implements Counter { + constructor(delegateFactory: () => Counter | undefined) { + super(delegateFactory, NOOP_COUNTER_METRIC); + } + + add(...args: Parameters) { + this._getDelegate().add(...args); + } + + protected _onDelegateAttached(): void {} +} + +class ProxyUpDownCounter + extends ProxyInstrumentBase + implements UpDownCounter +{ + constructor(delegateFactory: () => UpDownCounter | undefined) { + super(delegateFactory, NOOP_UP_DOWN_COUNTER_METRIC); + } + + add(...args: Parameters) { + this._getDelegate().add(...args); + } + + protected _onDelegateAttached(): void {} +} + +class ProxyGauge extends ProxyInstrumentBase implements Gauge { + constructor(delegateFactory: () => Gauge | undefined) { + super(delegateFactory, NOOP_GAUGE_METRIC); + } + + record(...args: Parameters) { + this._getDelegate().record(...args); + } + + protected _onDelegateAttached(): void {} +} + +class ProxyHistogram + extends ProxyInstrumentBase + implements Histogram +{ + constructor(delegateFactory: () => Histogram | undefined) { + super(delegateFactory, NOOP_HISTOGRAM_METRIC); + } + + record(...args: Parameters) { + this._getDelegate().record(...args); + } + + protected _onDelegateAttached(): void {} +} + +abstract class ProxyObservableInstrument + extends ProxyInstrumentBase + implements Observable +{ + private readonly _callbacks = new Set(); + + addCallback(callback: ObservableCallback): void { + this._callbacks.add(callback); + this._getDelegate().addCallback(callback); + } + + removeCallback(callback: ObservableCallback): void { + this._callbacks.delete(callback); + this._getDelegate().removeCallback(callback); + } + + protected _onDelegateAttached(delegate: T): void { + for (const callback of this._callbacks) { + delegate.addCallback(callback); + } + } +} + +class ProxyObservableGauge + extends ProxyObservableInstrument + implements ObservableGauge +{ + constructor(delegateFactory: () => ObservableGauge | undefined) { + super(delegateFactory, NOOP_OBSERVABLE_GAUGE_METRIC); + } +} + +class ProxyObservableCounter + extends ProxyObservableInstrument + implements ObservableCounter +{ + constructor(delegateFactory: () => ObservableCounter | undefined) { + super(delegateFactory, NOOP_OBSERVABLE_COUNTER_METRIC); + } +} + +class ProxyObservableUpDownCounter + extends ProxyObservableInstrument + implements ObservableUpDownCounter +{ + constructor(delegateFactory: () => ObservableUpDownCounter | undefined) { + super(delegateFactory, NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC); + } +} + +interface MeterDelegator { + getDelegateMeter( + name: string, + version?: string, + options?: MeterOptions + ): Meter | undefined; + _onProxyMeterDelegateBound(meter: ProxyMeter): void; +} diff --git a/api/src/metrics/ProxyMeterProvider.ts b/api/src/metrics/ProxyMeterProvider.ts new file mode 100644 index 00000000000..24b8a511400 --- /dev/null +++ b/api/src/metrics/ProxyMeterProvider.ts @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MeterProvider } from './MeterProvider'; +import { ProxyMeter } from './ProxyMeter'; +import { NoopMeterProvider } from './NoopMeterProvider'; +import { Meter, MeterOptions } from './Meter'; + +const NOOP_METER_PROVIDER = new NoopMeterProvider(); + +/** + * Meter provider which provides {@link ProxyMeter}s. + * + * Before a delegate is set, meters provided are NoOp. + * When a delegate is set, meters are provided from the delegate. + * When a delegate is set after meters have already been provided, + * all meters already provided will use the provided delegate implementation. + */ +export class ProxyMeterProvider implements MeterProvider { + private _delegate?: MeterProvider; + private readonly _proxyMeters = new Set(); + + /** + * Get a {@link ProxyMeter} + */ + getMeter(name: string, version?: string, options?: MeterOptions): Meter { + const delegate = this.getDelegateMeter(name, version, options); + if (delegate) { + return delegate; + } + + const meter = new ProxyMeter(this, name, version, options); + this._proxyMeters.add(meter); + return meter; + } + + getDelegate(): MeterProvider { + return this._delegate ?? NOOP_METER_PROVIDER; + } + + /** + * Set the delegate meter provider + */ + setDelegate(delegate: MeterProvider) { + this._delegate = delegate; + for (const meter of this._proxyMeters) { + meter._bindDelegate(); + } + this._proxyMeters.clear(); + } + + getDelegateMeter( + name: string, + version?: string, + options?: MeterOptions + ): Meter | undefined { + return this._delegate?.getMeter(name, version, options); + } + + /** @internal */ + _onProxyMeterDelegateBound(meter: ProxyMeter): void { + this._proxyMeters.delete(meter); + } +} diff --git a/api/test/common/proxy-implementations/proxy-meter.test.ts b/api/test/common/proxy-implementations/proxy-meter.test.ts new file mode 100644 index 00000000000..44347e26fe6 --- /dev/null +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -0,0 +1,279 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { + ProxyMeterProvider, + Meter, + MeterProvider, + Histogram, + UpDownCounter, + ObservableGauge, + ObservableCounter, + Counter, + ObservableUpDownCounter, + Gauge, +} from '../../../src'; +import { ProxyMeter } from '../../../src/metrics/ProxyMeter'; +import { NoopMeter } from '../../../src/metrics/NoopMeter'; + +describe('ProxyMeter', () => { + let provider: ProxyMeterProvider; + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + provider = new ProxyMeterProvider(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('when no delegate is set', () => { + it('should return proxy meters', () => { + const meter = provider.getMeter('test'); + + assert.ok(meter instanceof ProxyMeter); + }); + + it('creates proxy instruments that act as no-ops before delegation', () => { + const meter = provider.getMeter('test'); + + const counter = meter.createCounter('counter'); + const histogram = meter.createHistogram('histogram'); + const observable = meter.createObservableGauge('gauge'); + + assert.doesNotThrow(() => counter.add(1)); + assert.doesNotThrow(() => histogram.record(1)); + assert.doesNotThrow(() => observable.addCallback(() => {})); + }); + }); + + describe('when delegate is set before getMeter', () => { + let delegate: MeterProvider; + let getMeterStub: sinon.SinonStub; + + beforeEach(() => { + getMeterStub = sandbox.stub().returns(new NoopMeter()); + delegate = { + getMeter: getMeterStub, + }; + provider.setDelegate(delegate); + }); + + it('should return meters directly from the delegate', () => { + const meter = provider.getMeter('test', 'v0'); + + sandbox.assert.calledOnce(getMeterStub); + assert.strictEqual(getMeterStub.firstCall.returnValue, meter); + assert.deepStrictEqual(getMeterStub.firstCall.args, [ + 'test', + 'v0', + undefined, + ]); + }); + + it('should return meters directly from the delegate with schema url', () => { + const meter = provider.getMeter('test', 'v0', { + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0', + }); + + sandbox.assert.calledOnce(getMeterStub); + assert.strictEqual(getMeterStub.firstCall.returnValue, meter); + assert.deepStrictEqual(getMeterStub.firstCall.args, [ + 'test', + 'v0', + { schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' }, + ]); + }); + }); + + describe('when delegate is set after getMeter', () => { + let meter: Meter; + let delegate: MeterProvider; + let delegateMeter: Meter; + let delegateGauge: Gauge; + let delegateHistogram: Histogram; + let delegateCounter: Counter; + let delegateUpDownCounter: UpDownCounter; + let delegateObservableGauge: ObservableGauge; + let delegateObservableCounter: ObservableCounter; + let delegateObservableUpDownCounter: ObservableUpDownCounter; + let addBatchStub: sinon.SinonStub; + let removeBatchStub: sinon.SinonStub; + + beforeEach(() => { + delegateGauge = { record: sandbox.stub() }; + delegateHistogram = { record: sandbox.stub() }; + delegateCounter = { add: sandbox.stub() }; + delegateUpDownCounter = { add: sandbox.stub() }; + delegateObservableGauge = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + delegateObservableCounter = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + delegateObservableUpDownCounter = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + addBatchStub = sandbox.stub(); + removeBatchStub = sandbox.stub(); + + delegateMeter = { + createGauge: sandbox.stub().returns(delegateGauge), + createHistogram: sandbox.stub().returns(delegateHistogram), + createCounter: sandbox.stub().returns(delegateCounter), + createObservableCounter: sandbox + .stub() + .returns(delegateObservableCounter), + createObservableGauge: sandbox.stub().returns(delegateObservableGauge), + createObservableUpDownCounter: sandbox + .stub() + .returns(delegateObservableUpDownCounter), + createUpDownCounter: sandbox.stub().returns(delegateUpDownCounter), + addBatchObservableCallback: addBatchStub, + removeBatchObservableCallback: removeBatchStub, + }; + + meter = provider.getMeter('test'); + + delegate = { + getMeter() { + return delegateMeter; + }, + }; + provider.setDelegate(delegate); + }); + + it('should create gauges using the delegate meter', () => { + const instrument = meter.createGauge('test'); + assert.strictEqual(instrument, delegateGauge); + }); + + it('should create histograms using the delegate meter', () => { + const instrument = meter.createHistogram('test'); + assert.strictEqual(instrument, delegateHistogram); + }); + + it('should create counters using the delegate meter', () => { + const instrument = meter.createCounter('test'); + assert.strictEqual(instrument, delegateCounter); + }); + + it('should create observable counters using the delegate meter', () => { + const instrument = meter.createObservableCounter('test'); + assert.strictEqual(instrument, delegateObservableCounter); + }); + + it('should create observable gauges using the delegate meter', () => { + const instrument = meter.createObservableGauge('test'); + assert.strictEqual(instrument, delegateObservableGauge); + }); + + it('should create observable up down counters using the delegate meter', () => { + const instrument = meter.createObservableUpDownCounter('test'); + assert.strictEqual(instrument, delegateObservableUpDownCounter); + }); + + it('should create up down counters using the delegate meter', () => { + const instrument = meter.createUpDownCounter('test'); + assert.strictEqual(instrument, delegateUpDownCounter); + }); + }); + + describe('when instruments are created before delegate is set', () => { + it('hydrates synchronous instruments once the delegate registers', () => { + const meter = provider.getMeter('test'); + const counter = meter.createCounter('pre-counter'); + const addStub = sandbox.stub(); + const delegateCounter: Counter = { + add: addStub, + }; + const delegateMeter = new NoopMeter(); + sandbox.stub(delegateMeter, 'createCounter').returns(delegateCounter); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + counter.add(7); + sandbox.assert.calledOnceWithExactly(addStub, 7); + }); + + it('hydrates observable callbacks that were added before delegation', () => { + const meter = provider.getMeter('test'); + const observable = meter.createObservableGauge('observable'); + const callback = sandbox.stub(); + observable.addCallback(callback); + + const delegateObservable: ObservableGauge = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + const delegateMeter = new NoopMeter(); + sandbox + .stub(delegateMeter, 'createObservableGauge') + .returns(delegateObservable); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + sandbox.assert.calledOnceWithExactly( + delegateObservable.addCallback as sinon.SinonStub, + callback + ); + }); + + it('hydrates batch observable callbacks registered before delegation', () => { + const meter = provider.getMeter('test'); + const observable = meter.createObservableGauge('batch'); + const callback = sandbox.stub(); + meter.addBatchObservableCallback(callback, [observable]); + + const delegateObservable: ObservableGauge = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + const delegateMeter = new NoopMeter(); + sandbox + .stub(delegateMeter, 'createObservableGauge') + .returns(delegateObservable); + const addBatchStub = sandbox.stub( + delegateMeter, + 'addBatchObservableCallback' + ); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + sandbox.assert.calledOnce(addBatchStub); + const [, registeredObservables] = addBatchStub.firstCall.args; + assert.strictEqual(registeredObservables[0], delegateObservable); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index 527b1cdedba..6bcc5b0ad05 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -113,11 +113,19 @@ function getValueInMillis(envName: string, defaultValue: number): number { */ function configureMetricProviderFromEnv(): IMetricReader[] { const metricReaders: IMetricReader[] = []; + const metricsExporterEnvDefined = Object.prototype.hasOwnProperty.call( + process.env, + 'OTEL_METRICS_EXPORTER' + ); const enabledExporters = Array.from( new Set(getStringListFromEnv('OTEL_METRICS_EXPORTER') ?? []) ); if (enabledExporters.length === 0) { + if (!metricsExporterEnvDefined) { + return metricReaders; + } + diag.debug('OTEL_METRICS_EXPORTER is empty. Using default otlp exporter.'); enabledExporters.push('otlp'); } diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 3314f613a20..dff9bf83229 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -18,6 +18,7 @@ import { context, propagation, ProxyTracerProvider, + ProxyMeterProvider, trace, diag, DiagLogLevel, @@ -101,6 +102,7 @@ function assertDefaultPropagatorRegistered() { describe('Node SDK', () => { let delegate: any; + let metricsDelegate: any; let logsDelegate: any; beforeEach(() => { @@ -112,6 +114,9 @@ describe('Node SDK', () => { logs.disable(); delegate = (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(); + metricsDelegate = ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate(); logsDelegate = ( logs.getLoggerProvider() as ProxyLoggerProvider )._getDelegate(); @@ -150,6 +155,12 @@ describe('Node SDK', () => { delegate, 'tracer provider should not have changed' ); + assert.strictEqual( + (metrics.getMeterProvider() as ProxyMeterProvider).getDelegate(), + metricsDelegate, + 'meter provider should not have changed' + ); + assert.ok(!(logs.getLoggerProvider() instanceof LoggerProvider)); assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); assert.strictEqual( (logs.getLoggerProvider() as ProxyLoggerProvider)._getDelegate(), @@ -201,6 +212,13 @@ describe('Node SDK', () => { sdk.start(); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); assertDefaultContextManagerRegistered(); assertDefaultPropagatorRegistered(); @@ -218,6 +236,13 @@ describe('Node SDK', () => { sdk.start(); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); assertDefaultContextManagerRegistered(); assertDefaultPropagatorRegistered(); @@ -241,6 +266,13 @@ describe('Node SDK', () => { sdk.start(); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); assertDefaultContextManagerRegistered(); assertDefaultPropagatorRegistered(); @@ -298,7 +330,11 @@ describe('Node SDK', () => { 'tracer provider should not have changed' ); - assert.ok(metrics.getMeterProvider() instanceof MeterProvider); + assert.ok( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ); await sdk.shutdown(); }); @@ -337,8 +373,9 @@ describe('Node SDK', () => { delegate, 'tracer provider should not have changed' ); - - const meterProvider = metrics.getMeterProvider() as MeterProvider; + const meterProvider = ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() as MeterProvider; assert.ok(meterProvider instanceof MeterProvider); // Verify that both metric readers are registered @@ -374,7 +411,11 @@ describe('Node SDK', () => { "The 'metricReader' option is deprecated. Please use 'metricReaders' instead." ); - assert.ok(metrics.getMeterProvider() instanceof MeterProvider); + assert.ok( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ); await sdk.shutdown(); }); @@ -405,7 +446,11 @@ describe('Node SDK', () => { "The 'metricReader' option is deprecated. Please use 'metricReaders' instead." ); - assert.ok(metrics.getMeterProvider() instanceof MeterProvider); + assert.ok( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ); await sdk.shutdown(); }); @@ -619,8 +664,8 @@ describe('Node SDK', () => { 'tracer provider should not have changed' ); - const meterProvider = metrics.getMeterProvider() as MeterProvider; - assert.ok(meterProvider); + const meterProvider = metrics.getMeterProvider() as ProxyMeterProvider; + assert.ok(meterProvider.getDelegate() instanceof MeterProvider); const meter = meterProvider.getMeter('NodeSDKViews', '1.0.0'); const counter = meter.createCounter('test_counter', { @@ -1017,7 +1062,13 @@ describe('Node SDK', () => { }); sdk.start(); - assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); await sdk.shutdown(); }); @@ -1252,12 +1303,34 @@ describe('Node SDK', () => { await sdk.shutdown(); }); + it('should not configure metrics when OTEL_METRICS_EXPORTER is unset', async () => { + delete process.env.OTEL_METRICS_EXPORTER; + process.env.OTEL_TRACES_EXPORTER = 'none'; + + const sdk = new NodeSDK(); + sdk.start(); + + try { + const meterProvider = metrics.getMeterProvider() as ProxyMeterProvider; + assert.strictEqual((meterProvider as any)._delegate, undefined); + + const meter = metrics.getMeter('proxy-meter-test'); + const counter = meter.createCounter('proxy-counter-test'); + counter.add(1); + } finally { + await sdk.shutdown(); + delete process.env.OTEL_TRACES_EXPORTER; + } + }); + it('should use console with default interval and timeout', async () => { process.env.OTEL_METRICS_EXPORTER = 'console'; const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof ConsoleMetricExporter @@ -1279,7 +1352,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPGrpcMetricExporter @@ -1301,7 +1376,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPProtoMetricExporter @@ -1323,7 +1400,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPHttpMetricExporter @@ -1345,7 +1424,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPGrpcMetricExporter @@ -1367,7 +1448,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPProtoMetricExporter @@ -1389,7 +1472,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPProtoMetricExporter @@ -1413,7 +1498,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPProtoMetricExporter @@ -1435,7 +1522,9 @@ describe('Node SDK', () => { const sdk = new NodeSDK(); sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader instanceof PrometheusMetricExporter @@ -1449,7 +1538,9 @@ describe('Node SDK', () => { sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok( sharedState.metricCollectors[0]._metricReader._exporter instanceof OTLPProtoMetricExporter @@ -1463,7 +1554,9 @@ describe('Node SDK', () => { sdk.start(); const meterProvider = metrics.getMeterProvider(); - const sharedState = (meterProvider as any)['_sharedState']; + const sharedState = ( + (meterProvider as ProxyMeterProvider).getDelegate() as any + )['_sharedState']; assert.ok(sharedState.metricCollectors.length === 2); assert.ok(