From 4e0fc68562197449c68f80b063197c357e5f078d Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:49:58 -0700 Subject: [PATCH 01/15] Delegating no-op provider --- api/src/api/metrics.ts | 17 +- api/src/index.ts | 2 + api/src/metrics/ProxyMeter.ts | 148 +++++++++++++ api/src/metrics/ProxyMeterProvider.ts | 63 ++++++ .../proxy-implementations/proxy-meter.test.ts | 198 ++++++++++++++++++ .../opentelemetry-sdk-node/test/sdk.test.ts | 55 ++++- 6 files changed, 470 insertions(+), 13 deletions(-) create mode 100644 api/src/metrics/ProxyMeter.ts create mode 100644 api/src/metrics/ProxyMeterProvider.ts create mode 100644 api/test/common/proxy-implementations/proxy-meter.test.ts 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 c5dbe1685bf..bf60cebeeeb 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -37,6 +37,8 @@ export { export type { DiagAPI } from './api/diag'; // Metrics APIs +export { ProxyMeter, MeterDelegator } from './metrics/ProxyMeter'; +export { ProxyMeterProvider } from './metrics/ProxyMeterProvider'; export { createNoopMeter } from './metrics/NoopMeter'; export { MeterOptions, Meter } from './metrics/Meter'; export { MeterProvider } from './metrics/MeterProvider'; diff --git a/api/src/metrics/ProxyMeter.ts b/api/src/metrics/ProxyMeter.ts new file mode 100644 index 00000000000..d524080419f --- /dev/null +++ b/api/src/metrics/ProxyMeter.ts @@ -0,0 +1,148 @@ +/* + * 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 { NoopMeter } from './NoopMeter'; +import { + BatchObservableCallback, + Counter, + Histogram, + MetricOptions, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, + Observable, +} from './Metric'; + +const NOOP_METER = new NoopMeter(); + +/** + * Proxy meter provided by the proxy meter provider + */ +export class ProxyMeter implements Meter { + // When a real implementation is provided, this will be it + private _delegate?: Meter; + + constructor( + private _provider: MeterDelegator, + public readonly name: string, + public readonly version?: string, + public readonly options?: MeterOptions + ) {} + + /** + * @see {@link Meter.createUpDownCounter} + */ + createHistogram(_name: string, _options?: MetricOptions): Histogram { + return this._getMeter().createHistogram(_name, _options); + } + + /** + * @see {@link Meter.createUpDownCounter} + */ + createCounter(_name: string, _options?: MetricOptions): Counter { + return this._getMeter().createCounter(_name, _options); + } + + /** + * @see {@link Meter.createUpDownCounter} + */ + createUpDownCounter(_name: string, _options?: MetricOptions): UpDownCounter { + return this._getMeter().createUpDownCounter(_name, _options); + } + + /** + * @see {@link Meter.createObservableGauge} + */ + createObservableGauge( + _name: string, + _options?: MetricOptions + ): ObservableGauge { + return this._getMeter().createObservableGauge(_name, _options); + } + + /** + * @see {@link Meter.createObservableCounter} + */ + createObservableCounter( + _name: string, + _options?: MetricOptions + ): ObservableCounter { + return this._getMeter().createObservableCounter(_name, _options); + } + + /** + * @see {@link Meter.createObservableUpDownCounter} + */ + createObservableUpDownCounter( + _name: string, + _options?: MetricOptions + ): ObservableUpDownCounter { + return this._getMeter().createObservableUpDownCounter(_name, _options); + } + + /** + * @see {@link Meter.addBatchObservableCallback} + */ + addBatchObservableCallback( + _callback: BatchObservableCallback, + _observables: Observable[] + ): void { + this._getMeter().addBatchObservableCallback(_callback, _observables); + } + + /** + * @see {@link Meter.removeBatchObservableCallback} + */ + removeBatchObservableCallback( + _callback: BatchObservableCallback, + _observables: Observable[] + ): void { + this._getMeter().removeBatchObservableCallback(_callback, _observables); + } + + /** + * Try to get a meter from the proxy meter provider. + * If the proxy meter provider has no delegate, return a noop meter. + */ + private _getMeter() { + if (this._delegate) { + return this._delegate; + } + + const meter = this._provider.getDelegateMeter( + this.name, + this.version, + this.options + ); + + if (!meter) { + return NOOP_METER; + } + + this._delegate = meter; + return this._delegate; + } +} + +export interface MeterDelegator { + getDelegateMeter( + name: string, + version?: string, + options?: MeterOptions + ): Meter | undefined; +} diff --git a/api/src/metrics/ProxyMeterProvider.ts b/api/src/metrics/ProxyMeterProvider.ts new file mode 100644 index 00000000000..bbfbe1cdf04 --- /dev/null +++ b/api/src/metrics/ProxyMeterProvider.ts @@ -0,0 +1,63 @@ +/* + * 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; + + /** + * Get a {@link ProxyMeter} + */ + getMeter(name: string, version?: string, options?: MeterOptions): Meter { + return ( + this.getDelegateMeter(name, version, options) ?? + new ProxyMeter(this, name, version, options) + ); + } + + getDelegate(): MeterProvider { + return this._delegate ?? NOOP_METER_PROVIDER; + } + + /** + * Set the delegate meter provider + */ + setDelegate(delegate: MeterProvider) { + this._delegate = delegate; + } + + getDelegateMeter( + name: string, + version?: string, + options?: MeterOptions + ): Meter | undefined { + return this._delegate?.getMeter(name, version, options); + } +} 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..f050592f4b9 --- /dev/null +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { + ProxyMeter, + ProxyMeterProvider, + Meter, + MeterProvider, + Histogram, + UpDownCounter, + ObservableGauge, + ObservableCounter, + Counter, + ObservableUpDownCounter, +} from '../../../src'; +import { + NoopHistogramMetric, + NoopMeter, + NoopObservableCounterMetric, + NoopObservableGaugeMetric, + NoopObservableUpDownCounterMetric, + NoopUpDownCounterMetric, +} 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('create instruments should return Noop metric instruments', () => { + const meter = provider.getMeter('test'); + assert.ok( + meter.createHistogram('histogram-name') instanceof NoopHistogramMetric + ); + assert.ok( + meter.createObservableCounter('observablecounter-name') instanceof + NoopObservableCounterMetric + ); + assert.ok( + meter.createObservableGauge('observableGauge-name') instanceof + NoopObservableGaugeMetric + ); + assert.ok( + meter.createObservableUpDownCounter('observableCounter-name') instanceof + NoopObservableUpDownCounterMetric + ); + assert.ok( + meter.createUpDownCounter('upDownCounter-name') instanceof + NoopUpDownCounterMetric + ); + }); + }); + + 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 delegateHistogram: Histogram; + let delegateCounter: Counter; + let delegateUpDownCounter: UpDownCounter; + let delegateObservableGauge: ObservableGauge; + let delegateObservableCounter: ObservableCounter; + let delegateObservableUpDownCounter: ObservableUpDownCounter; + let delegateMeter: Meter; + + beforeEach(() => { + delegateHistogram = new NoopHistogramMetric(); + delegateMeter = { + createHistogram() { + return delegateHistogram; + }, + createCounter() { + return delegateCounter; + }, + createObservableCounter() { + return delegateObservableCounter; + }, + createObservableGauge() { + return delegateObservableGauge; + }, + createObservableUpDownCounter() { + return delegateObservableUpDownCounter; + }, + createUpDownCounter() { + return delegateUpDownCounter; + }, + addBatchObservableCallback() {}, + removeBatchObservableCallback() {}, + }; + + meter = provider.getMeter('test'); + + delegate = { + getMeter() { + return delegateMeter; + }, + }; + provider.setDelegate(delegate); + }); + + 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 observable up down counters using the delegate meter', () => { + const histogram = meter.createUpDownCounter('test'); + assert.strictEqual(histogram, delegateUpDownCounter); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 22e1794ccc6..bb11f9fab1b 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, @@ -84,6 +85,7 @@ describe('Node SDK', () => { let ctxManager: any; let propagator: any; let delegate: any; + let metricsDelegate: any; beforeEach(() => { diag.disable(); @@ -95,6 +97,9 @@ describe('Node SDK', () => { ctxManager = context['_getContextManager'](); propagator = propagation['_getGlobalPropagator'](); delegate = (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(); + metricsDelegate = ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate(); }); afterEach(() => { @@ -127,7 +132,11 @@ describe('Node SDK', () => { delegate, 'tracer provider should not have changed' ); - assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.strictEqual( + (metrics.getMeterProvider() as ProxyMeterProvider).getDelegate(), + metricsDelegate, + 'meter provider should not have changed' + ); assert.ok(!(logs.getLoggerProvider() instanceof LoggerProvider)); delete env.OTEL_TRACES_EXPORTER; await sdk.shutdown(); @@ -175,7 +184,13 @@ describe('Node SDK', () => { sdk.start(); - assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); assert.ok( context['_getContextManager']().constructor.name === @@ -198,7 +213,13 @@ describe('Node SDK', () => { sdk.start(); - assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); assert.ok( context['_getContextManager']().constructor.name === @@ -228,7 +249,13 @@ describe('Node SDK', () => { sdk.start(); - assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); assert.ok( context['_getContextManager']().constructor.name === @@ -285,9 +312,11 @@ describe('Node SDK', () => { delegate, 'tracer provider should not have changed' ); - - assert.ok(metrics.getMeterProvider() instanceof MeterProvider); - + assert.ok( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ); await sdk.shutdown(); delete env.OTEL_TRACES_EXPORTER; }); @@ -386,8 +415,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', { @@ -768,7 +797,13 @@ describe('Node SDK', () => { }); sdk.start(); - assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.ok( + !( + ( + metrics.getMeterProvider() as ProxyMeterProvider + ).getDelegate() instanceof MeterProvider + ) + ); await sdk.shutdown(); }); From 2c8b47647c0b057896ea5d45af86ffb15ef9feca Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:01:25 -0700 Subject: [PATCH 02/15] add create gauge methods --- api/src/metrics/ProxyMeter.ts | 8 ++++++++ .../common/proxy-implementations/proxy-meter.test.ts | 10 ++++++++++ experimental/packages/otlp-grpc-exporter-base/protos | 1 + experimental/packages/otlp-proto-exporter-base/protos | 1 + 4 files changed, 20 insertions(+) create mode 160000 experimental/packages/otlp-grpc-exporter-base/protos create mode 160000 experimental/packages/otlp-proto-exporter-base/protos diff --git a/api/src/metrics/ProxyMeter.ts b/api/src/metrics/ProxyMeter.ts index d524080419f..0e7f930b2b7 100644 --- a/api/src/metrics/ProxyMeter.ts +++ b/api/src/metrics/ProxyMeter.ts @@ -26,6 +26,7 @@ import { ObservableUpDownCounter, UpDownCounter, Observable, + Gauge, } from './Metric'; const NOOP_METER = new NoopMeter(); @@ -44,6 +45,13 @@ export class ProxyMeter implements Meter { public readonly options?: MeterOptions ) {} + /** + * @see {@link Meter.createGauge} + */ + createGauge(_name: string, _options?: MetricOptions): Gauge { + return this._getMeter().createGauge(_name, _options); + } + /** * @see {@link Meter.createUpDownCounter} */ diff --git a/api/test/common/proxy-implementations/proxy-meter.test.ts b/api/test/common/proxy-implementations/proxy-meter.test.ts index f050592f4b9..6007112976c 100644 --- a/api/test/common/proxy-implementations/proxy-meter.test.ts +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -27,6 +27,7 @@ import { ObservableCounter, Counter, ObservableUpDownCounter, + Gauge, } from '../../../src'; import { NoopHistogramMetric, @@ -122,6 +123,7 @@ describe('ProxyMeter', () => { describe('when delegate is set after getMeter', () => { let meter: Meter; let delegate: MeterProvider; + let delegateGauge: Gauge; let delegateHistogram: Histogram; let delegateCounter: Counter; let delegateUpDownCounter: UpDownCounter; @@ -133,6 +135,9 @@ describe('ProxyMeter', () => { beforeEach(() => { delegateHistogram = new NoopHistogramMetric(); delegateMeter = { + createGauge() { + return delegateGauge; + }, createHistogram() { return delegateHistogram; }, @@ -165,6 +170,11 @@ describe('ProxyMeter', () => { 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); diff --git a/experimental/packages/otlp-grpc-exporter-base/protos b/experimental/packages/otlp-grpc-exporter-base/protos new file mode 160000 index 00000000000..1608f92cf08 --- /dev/null +++ b/experimental/packages/otlp-grpc-exporter-base/protos @@ -0,0 +1 @@ +Subproject commit 1608f92cf08119f9aec237c910b200d1317ec696 diff --git a/experimental/packages/otlp-proto-exporter-base/protos b/experimental/packages/otlp-proto-exporter-base/protos new file mode 160000 index 00000000000..1608f92cf08 --- /dev/null +++ b/experimental/packages/otlp-proto-exporter-base/protos @@ -0,0 +1 @@ +Subproject commit 1608f92cf08119f9aec237c910b200d1317ec696 From fa9d8dccbb9404fe39ba3cf2517ca27102b8b84c Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:04:13 -0700 Subject: [PATCH 03/15] Remove otlp protos --- experimental/packages/otlp-grpc-exporter-base/protos | 1 - experimental/packages/otlp-proto-exporter-base/protos | 1 - 2 files changed, 2 deletions(-) delete mode 160000 experimental/packages/otlp-grpc-exporter-base/protos delete mode 160000 experimental/packages/otlp-proto-exporter-base/protos diff --git a/experimental/packages/otlp-grpc-exporter-base/protos b/experimental/packages/otlp-grpc-exporter-base/protos deleted file mode 160000 index 1608f92cf08..00000000000 --- a/experimental/packages/otlp-grpc-exporter-base/protos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1608f92cf08119f9aec237c910b200d1317ec696 diff --git a/experimental/packages/otlp-proto-exporter-base/protos b/experimental/packages/otlp-proto-exporter-base/protos deleted file mode 160000 index 1608f92cf08..00000000000 --- a/experimental/packages/otlp-proto-exporter-base/protos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1608f92cf08119f9aec237c910b200d1317ec696 From 7fafc7a08ea29060130026553932abbfeaceb0d7 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:40:26 -0700 Subject: [PATCH 04/15] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 644972b9503..6e5d608d531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ### :boom: Breaking Change +* feat(api) Add delegating no-op meter provider [#4858](https://github.com/open-telemetry/opentelemetry-js/pull/4858) @hectorhdzg + ### :rocket: (Enhancement) ### :bug: (Bug Fix) From 55fe56617e51f8914394d7ec8f0017134237abee Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:20:31 -0700 Subject: [PATCH 05/15] Update API changelog --- CHANGELOG.md | 2 -- api/CHANGELOG.md | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5d608d531..644972b9503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,6 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ### :boom: Breaking Change -* feat(api) Add delegating no-op meter provider [#4858](https://github.com/open-telemetry/opentelemetry-js/pull/4858) @hectorhdzg - ### :rocket: (Enhancement) ### :bug: (Bug Fix) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 860f839c371..990a3e1c814 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -7,6 +7,8 @@ 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 + ### :rocket: (Enhancement) ### :bug: (Bug Fix) From 35b02b4e4d230e7c5711ccfdf34c1eaccfe9a186 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:53:20 -0700 Subject: [PATCH 06/15] Update --- api/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/index.ts b/api/src/index.ts index 7d36c19ab31..3f5849791de 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -45,7 +45,7 @@ export type { export type { DiagAPI } from './api/diag'; // Metrics APIs -export { ProxyMeter, MeterDelegator } from './metrics/ProxyMeter'; +export { ProxyMeter, type MeterDelegator } from './metrics/ProxyMeter'; export { ProxyMeterProvider } from './metrics/ProxyMeterProvider'; export { createNoopMeter } from './metrics/NoopMeter'; export type { MeterOptions, Meter } from './metrics/Meter'; From 41c1af4d4fc9a72a7436c24f5d023d4018ae721e Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:43:03 -0700 Subject: [PATCH 07/15] Update tests --- api/src/metrics/ProxyMeter.ts | 12 +++---- .../opentelemetry-sdk-node/test/sdk.test.ts | 36 ++++++++++++++----- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/api/src/metrics/ProxyMeter.ts b/api/src/metrics/ProxyMeter.ts index 0e7f930b2b7..de06928f758 100644 --- a/api/src/metrics/ProxyMeter.ts +++ b/api/src/metrics/ProxyMeter.ts @@ -40,9 +40,9 @@ export class ProxyMeter implements Meter { constructor( private _provider: MeterDelegator, - public readonly name: string, - public readonly version?: string, - public readonly options?: MeterOptions + private readonly _name: string, + private readonly _version?: string, + private readonly _options?: MeterOptions ) {} /** @@ -133,9 +133,9 @@ export class ProxyMeter implements Meter { } const meter = this._provider.getDelegateMeter( - this.name, - this.version, - this.options + this._name, + this._version, + this._options ); if (!meter) { diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index f2cb15a4eae..b5c7d8683e4 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -1164,7 +1164,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 ConsoleMetricExporter @@ -1186,7 +1188,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 @@ -1208,7 +1212,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 @@ -1230,7 +1236,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 @@ -1252,7 +1260,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 @@ -1274,7 +1284,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 @@ -1296,7 +1308,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 @@ -1321,7 +1335,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 @@ -1345,7 +1361,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 From 25a20824270da71960e9df1f40ccd204b6558bf8 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:29:36 -0800 Subject: [PATCH 08/15] Adding proxy instruments --- api/CHANGELOG.md | 1 + api/src/index.ts | 1 - api/src/metrics/ProxyMeter.ts | 369 ++++++++++++++++-- api/src/metrics/ProxyMeterProvider.ts | 22 +- .../proxy-implementations/proxy-meter.test.ts | 185 ++++++--- 5 files changed, 483 insertions(+), 95 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 20242a2e9f3..86b959f769e 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -8,6 +8,7 @@ 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) diff --git a/api/src/index.ts b/api/src/index.ts index 3f5849791de..c8ac1f8dda6 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -45,7 +45,6 @@ export type { export type { DiagAPI } from './api/diag'; // Metrics APIs -export { ProxyMeter, type MeterDelegator } from './metrics/ProxyMeter'; export { ProxyMeterProvider } from './metrics/ProxyMeterProvider'; export { createNoopMeter } from './metrics/NoopMeter'; export type { MeterOptions, Meter } from './metrics/Meter'; diff --git a/api/src/metrics/ProxyMeter.ts b/api/src/metrics/ProxyMeter.ts index de06928f758..9d60f96f2fc 100644 --- a/api/src/metrics/ProxyMeter.ts +++ b/api/src/metrics/ProxyMeter.ts @@ -15,31 +15,45 @@ */ import { Meter, MeterOptions } from './Meter'; -import { NoopMeter } from './NoopMeter'; +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, - Observable, - Gauge, } from './Metric'; -const NOOP_METER = new NoopMeter(); +const INTERNAL_NOOP_METER = NOOP_METER; /** * Proxy meter provided by the proxy meter provider */ export class ProxyMeter implements Meter { - // When a real implementation is provided, this will be it private _delegate?: Meter; + private readonly _instruments = new Set>(); + private readonly _batchCallbacks = new Map< + BatchObservableCallback, + Observable[] + >(); constructor( - private _provider: MeterDelegator, + private readonly _provider: MeterDelegator, private readonly _name: string, private readonly _version?: string, private readonly _options?: MeterOptions @@ -48,79 +62,209 @@ export class ProxyMeter implements Meter { /** * @see {@link Meter.createGauge} */ - createGauge(_name: string, _options?: MetricOptions): Gauge { - return this._getMeter().createGauge(_name, _options); + 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.createUpDownCounter} + * @see {@link Meter.createHistogram} */ - createHistogram(_name: string, _options?: MetricOptions): Histogram { - return this._getMeter().createHistogram(_name, _options); + 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.createUpDownCounter} + * @see {@link Meter.createCounter} */ - createCounter(_name: string, _options?: MetricOptions): Counter { - return this._getMeter().createCounter(_name, _options); + 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 { - return this._getMeter().createUpDownCounter(_name, _options); + 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 + name: string, + options?: MetricOptions ): ObservableGauge { - return this._getMeter().createObservableGauge(_name, _options); + 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 + name: string, + options?: MetricOptions ): ObservableCounter { - return this._getMeter().createObservableCounter(_name, _options); + 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 + name: string, + options?: MetricOptions ): ObservableUpDownCounter { - return this._getMeter().createObservableUpDownCounter(_name, _options); + 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[] + callback: BatchObservableCallback, + observables: Observable[] ): void { - this._getMeter().addBatchObservableCallback(_callback, _observables); + 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[] + callback: BatchObservableCallback, + observables: Observable[] ): void { - this._getMeter().removeBatchObservableCallback(_callback, _observables); + 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(); } /** @@ -128,6 +272,10 @@ export class ProxyMeter implements Meter { * 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; } @@ -139,18 +287,173 @@ export class ProxyMeter implements Meter { ); if (!meter) { - return NOOP_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); + } } -export interface MeterDelegator { +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 index bbfbe1cdf04..24b8a511400 100644 --- a/api/src/metrics/ProxyMeterProvider.ts +++ b/api/src/metrics/ProxyMeterProvider.ts @@ -31,15 +31,20 @@ const NOOP_METER_PROVIDER = new NoopMeterProvider(); */ 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 { - return ( - this.getDelegateMeter(name, version, options) ?? - new ProxyMeter(this, name, version, options) - ); + 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 { @@ -51,6 +56,10 @@ export class ProxyMeterProvider implements MeterProvider { */ setDelegate(delegate: MeterProvider) { this._delegate = delegate; + for (const meter of this._proxyMeters) { + meter._bindDelegate(); + } + this._proxyMeters.clear(); } getDelegateMeter( @@ -60,4 +69,9 @@ export class ProxyMeterProvider implements MeterProvider { ): 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 index 6007112976c..44347e26fe6 100644 --- a/api/test/common/proxy-implementations/proxy-meter.test.ts +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -17,7 +17,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { - ProxyMeter, ProxyMeterProvider, Meter, MeterProvider, @@ -29,14 +28,8 @@ import { ObservableUpDownCounter, Gauge, } from '../../../src'; -import { - NoopHistogramMetric, - NoopMeter, - NoopObservableCounterMetric, - NoopObservableGaugeMetric, - NoopObservableUpDownCounterMetric, - NoopUpDownCounterMetric, -} from '../../../src/metrics/NoopMeter'; +import { ProxyMeter } from '../../../src/metrics/ProxyMeter'; +import { NoopMeter } from '../../../src/metrics/NoopMeter'; describe('ProxyMeter', () => { let provider: ProxyMeterProvider; @@ -57,27 +50,16 @@ describe('ProxyMeter', () => { assert.ok(meter instanceof ProxyMeter); }); - it('create instruments should return Noop metric instruments', () => { + it('creates proxy instruments that act as no-ops before delegation', () => { const meter = provider.getMeter('test'); - assert.ok( - meter.createHistogram('histogram-name') instanceof NoopHistogramMetric - ); - assert.ok( - meter.createObservableCounter('observablecounter-name') instanceof - NoopObservableCounterMetric - ); - assert.ok( - meter.createObservableGauge('observableGauge-name') instanceof - NoopObservableGaugeMetric - ); - assert.ok( - meter.createObservableUpDownCounter('observableCounter-name') instanceof - NoopObservableUpDownCounterMetric - ); - assert.ok( - meter.createUpDownCounter('upDownCounter-name') instanceof - NoopUpDownCounterMetric - ); + + 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(() => {})); }); }); @@ -123,6 +105,7 @@ describe('ProxyMeter', () => { 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; @@ -130,34 +113,43 @@ describe('ProxyMeter', () => { let delegateObservableGauge: ObservableGauge; let delegateObservableCounter: ObservableCounter; let delegateObservableUpDownCounter: ObservableUpDownCounter; - let delegateMeter: Meter; + let addBatchStub: sinon.SinonStub; + let removeBatchStub: sinon.SinonStub; beforeEach(() => { - delegateHistogram = new NoopHistogramMetric(); + 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() { - return delegateGauge; - }, - createHistogram() { - return delegateHistogram; - }, - createCounter() { - return delegateCounter; - }, - createObservableCounter() { - return delegateObservableCounter; - }, - createObservableGauge() { - return delegateObservableGauge; - }, - createObservableUpDownCounter() { - return delegateObservableUpDownCounter; - }, - createUpDownCounter() { - return delegateUpDownCounter; - }, - addBatchObservableCallback() {}, - removeBatchObservableCallback() {}, + 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'); @@ -200,9 +192,88 @@ describe('ProxyMeter', () => { assert.strictEqual(instrument, delegateObservableUpDownCounter); }); - it('should create observable up down counters using the delegate meter', () => { - const histogram = meter.createUpDownCounter('test'); - assert.strictEqual(histogram, delegateUpDownCounter); + 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); }); }); }); From 1205b307fa81915dd81bedf6e75366450dfada0c Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:36:37 -0800 Subject: [PATCH 09/15] Update tests --- .../opentelemetry-sdk-node/test/sdk.test.ts | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 945c033cb7f..05752b38916 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -330,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(); }); @@ -369,13 +373,9 @@ describe('Node SDK', () => { delegate, 'tracer provider should not have changed' ); - assert.ok( - ( - metrics.getMeterProvider() as ProxyMeterProvider - ).getDelegate() instanceof MeterProvider - ); - - 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 @@ -411,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(); }); @@ -442,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(); }); @@ -1510,7 +1518,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 @@ -1524,7 +1534,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( From 133bdc8e4b09043413b4a0e4c2a1e95e4391a662 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:01:59 -0800 Subject: [PATCH 10/15] Update --- experimental/packages/opentelemetry-sdk-node/src/sdk.ts | 8 ++++++++ 1 file changed, 8 insertions(+) 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'); } From e06a96203f12ef5acbbbd6a61bab30c9a720d49c Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:30:55 -0800 Subject: [PATCH 11/15] Add test coverage --- .../opentelemetry-sdk-node/test/sdk.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 05752b38916..dff9bf83229 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -1303,6 +1303,26 @@ 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(); From 0f150fdc04f3d55bfb87cfa79e2911f531aad0b9 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:29:42 -0800 Subject: [PATCH 12/15] Add more tests --- .../proxy-implementations/proxy-meter.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/test/common/proxy-implementations/proxy-meter.test.ts b/api/test/common/proxy-implementations/proxy-meter.test.ts index 44347e26fe6..cb57213584d 100644 --- a/api/test/common/proxy-implementations/proxy-meter.test.ts +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -27,6 +27,7 @@ import { Counter, ObservableUpDownCounter, Gauge, + NoopMeterProvider, } from '../../../src'; import { ProxyMeter } from '../../../src/metrics/ProxyMeter'; import { NoopMeter } from '../../../src/metrics/NoopMeter'; @@ -43,6 +44,21 @@ describe('ProxyMeter', () => { sandbox.restore(); }); + describe('getDelegate', () => { + it('returns NoopMeterProvider when delegate is unset', () => { + const delegate = provider.getDelegate(); + assert.ok(delegate instanceof NoopMeterProvider); + }); + + it('returns the configured delegate when set', () => { + const noopDelegate = new NoopMeterProvider(); + provider.setDelegate(noopDelegate); + + const delegate = provider.getDelegate(); + assert.strictEqual(delegate, noopDelegate); + }); + }); + describe('when no delegate is set', () => { it('should return proxy meters', () => { const meter = provider.getMeter('test'); From b8db5fad26bf5919afb7dab6f4126100e2bcb95a Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:23:27 -0800 Subject: [PATCH 13/15] Fix build error --- api/test/common/proxy-implementations/proxy-meter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/common/proxy-implementations/proxy-meter.test.ts b/api/test/common/proxy-implementations/proxy-meter.test.ts index cb57213584d..67f66a2496b 100644 --- a/api/test/common/proxy-implementations/proxy-meter.test.ts +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -27,8 +27,8 @@ import { Counter, ObservableUpDownCounter, Gauge, - NoopMeterProvider, } from '../../../src'; +import { NoopMeterProvider } from '../../../src/metrics/NoopMeterProvider'; import { ProxyMeter } from '../../../src/metrics/ProxyMeter'; import { NoopMeter } from '../../../src/metrics/NoopMeter'; From c6ff769aca4c78e9eb67ed51efe46a945364dd50 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:06:58 -0800 Subject: [PATCH 14/15] Add more tests --- .../proxy-implementations/proxy-meter.test.ts | 357 +++++++++++++++++- 1 file changed, 356 insertions(+), 1 deletion(-) diff --git a/api/test/common/proxy-implementations/proxy-meter.test.ts b/api/test/common/proxy-implementations/proxy-meter.test.ts index 67f66a2496b..1de0a228686 100644 --- a/api/test/common/proxy-implementations/proxy-meter.test.ts +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -27,10 +27,11 @@ import { Counter, ObservableUpDownCounter, Gauge, + Observable, } from '../../../src'; import { NoopMeterProvider } from '../../../src/metrics/NoopMeterProvider'; import { ProxyMeter } from '../../../src/metrics/ProxyMeter'; -import { NoopMeter } from '../../../src/metrics/NoopMeter'; +import { NoopMeter, NOOP_METER } from '../../../src/metrics/NoopMeter'; describe('ProxyMeter', () => { let provider: ProxyMeterProvider; @@ -77,6 +78,36 @@ describe('ProxyMeter', () => { assert.doesNotThrow(() => histogram.record(1)); assert.doesNotThrow(() => observable.addCallback(() => {})); }); + + it('creates additional synchronous instruments that remain no-ops before delegation', () => { + const meter = provider.getMeter('test'); + + const gauge = meter.createGauge('gauge'); + const upDownCounter = meter.createUpDownCounter('upDown'); + + assert.doesNotThrow(() => gauge.record(5)); + assert.doesNotThrow(() => upDownCounter.add(-1)); + }); + + it('creates observable counters that buffer callbacks before delegation', () => { + const meter = provider.getMeter('test'); + + const observableCounter = meter.createObservableCounter('observable-counter'); + const observableUpDownCounter = meter.createObservableUpDownCounter( + 'observable-up-down-counter' + ); + const counterCallback = sandbox.stub(); + const upDownCallback = sandbox.stub(); + + assert.doesNotThrow(() => observableCounter.addCallback(counterCallback)); + assert.doesNotThrow(() => observableCounter.removeCallback(counterCallback)); + assert.doesNotThrow(() => + observableUpDownCounter.addCallback(upDownCallback) + ); + assert.doesNotThrow(() => + observableUpDownCounter.removeCallback(upDownCallback) + ); + }); }); describe('when delegate is set before getMeter', () => { @@ -212,6 +243,25 @@ describe('ProxyMeter', () => { const instrument = meter.createUpDownCounter('test'); assert.strictEqual(instrument, delegateUpDownCounter); }); + + it('registers batch callbacks through the delegate once bound', () => { + const proxyObservable = meter.createObservableGauge('proxy'); + const foreignObservable: ObservableGauge = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + const callback = sandbox.stub(); + + meter.addBatchObservableCallback(callback, [ + proxyObservable, + foreignObservable, + ]); + + sandbox.assert.calledOnce(addBatchStub); + const [, registeredObservables] = addBatchStub.firstCall.args; + assert.strictEqual(registeredObservables[0], delegateObservableGauge); + assert.strictEqual(registeredObservables[1], foreignObservable); + }); }); describe('when instruments are created before delegate is set', () => { @@ -235,6 +285,122 @@ describe('ProxyMeter', () => { sandbox.assert.calledOnceWithExactly(addStub, 7); }); + it('hydrates gauges that were created before delegation', () => { + const meter = provider.getMeter('test'); + const gauge = meter.createGauge('pre-gauge'); + const recordStub = sandbox.stub(); + const delegateGauge: Gauge = { + record: recordStub, + } as Gauge; + const delegateMeter = new NoopMeter(); + sandbox.stub(delegateMeter, 'createGauge').returns(delegateGauge); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + gauge.record(9); + sandbox.assert.calledOnceWithExactly(recordStub, 9); + }); + + it('hydrates histograms that were created before delegation', () => { + const meter = provider.getMeter('test'); + const histogram = meter.createHistogram('pre-histogram'); + const recordStub = sandbox.stub(); + const delegateHistogram: Histogram = { + record: recordStub, + } as Histogram; + const delegateMeter = new NoopMeter(); + sandbox.stub(delegateMeter, 'createHistogram').returns(delegateHistogram); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + histogram.record(33); + sandbox.assert.calledOnceWithExactly(recordStub, 33); + }); + + it('hydrates up down counters that were created before delegation', () => { + const meter = provider.getMeter('test'); + const upDownCounter = meter.createUpDownCounter('pre-updown'); + const addStub = sandbox.stub(); + const delegateUpDownCounter: UpDownCounter = { + add: addStub, + } as UpDownCounter; + const delegateMeter = new NoopMeter(); + sandbox.stub(delegateMeter, 'createUpDownCounter').returns(delegateUpDownCounter); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + upDownCounter.add(-11); + sandbox.assert.calledOnceWithExactly(addStub, -11); + }); + + it('hydrates observable counters that were created before delegation', () => { + const meter = provider.getMeter('test'); + const observableCounter = meter.createObservableCounter('pre-observable-counter'); + const callback = sandbox.stub(); + observableCounter.addCallback(callback); + + const delegateObservableCounter: ObservableCounter = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + const delegateMeter = new NoopMeter(); + sandbox + .stub(delegateMeter, 'createObservableCounter') + .returns(delegateObservableCounter); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + sandbox.assert.calledOnceWithExactly( + delegateObservableCounter.addCallback as sinon.SinonStub, + callback + ); + }); + + it('hydrates observable up down counters that were created before delegation', () => { + const meter = provider.getMeter('test'); + const observableUpDownCounter = meter.createObservableUpDownCounter( + 'pre-observable-updown' + ); + const callback = sandbox.stub(); + observableUpDownCounter.addCallback(callback); + + const delegateObservableUpDownCounter: ObservableUpDownCounter = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + const delegateMeter = new NoopMeter(); + sandbox + .stub(delegateMeter, 'createObservableUpDownCounter') + .returns(delegateObservableUpDownCounter); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + sandbox.assert.calledOnceWithExactly( + delegateObservableUpDownCounter.addCallback as sinon.SinonStub, + callback + ); + }); + it('hydrates observable callbacks that were added before delegation', () => { const meter = provider.getMeter('test'); const observable = meter.createObservableGauge('observable'); @@ -291,5 +457,194 @@ describe('ProxyMeter', () => { const [, registeredObservables] = addBatchStub.firstCall.args; assert.strictEqual(registeredObservables[0], delegateObservable); }); + + it('remaps proxy observables when registering batch callbacks after delegation', () => { + const meter = provider.getMeter('test'); + const proxyObservable = meter.createObservableGauge('proxy-batch'); + const callback = sandbox.stub(); + 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; + }, + }); + + meter.addBatchObservableCallback(callback, [proxyObservable]); + + sandbox.assert.calledOnce(addBatchStub); + const [, registeredObservables] = addBatchStub.firstCall.args; + assert.strictEqual(registeredObservables[0], delegateObservable); + }); + + it('removes batch callbacks via the current delegate before delegation', () => { + const meter = provider.getMeter('test'); + const observable = meter.createObservableGauge('batch'); + const callback = sandbox.stub(); + + meter.addBatchObservableCallback(callback, [observable]); + + const noopMeter = new NoopMeter(); + const getMeterStub = sandbox + .stub(meter as unknown as { _getMeter: () => Meter }, '_getMeter') + .returns(noopMeter); + sandbox.stub(noopMeter, 'removeBatchObservableCallback'); + + assert.doesNotThrow(() => + meter.removeBatchObservableCallback(callback, [observable]) + ); + sandbox.assert.calledOnce(getMeterStub); + sandbox.assert.calledOnceWithExactly( + noopMeter.removeBatchObservableCallback as sinon.SinonStub, + callback, + [observable] + ); + }); + + it('removes batch callbacks by delegating to the noop meter when unset', () => { + const meter = provider.getMeter('test'); + const observable = meter.createObservableGauge('noop-batch'); + const callback = sandbox.stub(); + meter.addBatchObservableCallback(callback, [observable]); + + const noopSpy = sandbox.spy(NOOP_METER, 'removeBatchObservableCallback'); + + meter.removeBatchObservableCallback(callback, [observable]); + + sandbox.assert.calledOnce(noopSpy); + }); + }); + + describe('proxy instrument internals', () => { + it('does not track instruments that already have delegates', () => { + const meter = provider.getMeter('test') as ProxyMeter; + const privateMeter = meter as unknown as { + _trackInstrument: (instrument: unknown) => void; + _instruments: Set; + }; + privateMeter._instruments.clear(); + const instrument = { + hasDelegate: sandbox.stub().returns(true), + bindDelegate: sandbox.stub(), + }; + + privateMeter._trackInstrument(instrument); + + assert.strictEqual(privateMeter._instruments.size, 0); + }); + + it('leaves non-proxy observables untouched when mapping delegates', () => { + const meter = provider.getMeter('test') as ProxyMeter; + const privateMeter = meter as unknown as { + _mapObservablesToDelegates: (observables: Observable[]) => Observable[]; + }; + const observable: Observable = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + + const [result] = privateMeter._mapObservablesToDelegates([observable]); + + assert.strictEqual(result, observable); + }); + + it('maps proxy observables to their delegates after binding', () => { + const meter = provider.getMeter('test') as ProxyMeter; + const privateMeter = meter as unknown as { + _mapObservablesToDelegates: (observables: Observable[]) => Observable[]; + }; + const proxyObservable = meter.createObservableGauge('proxy'); + const delegateObservable: ObservableGauge = { + addCallback: sandbox.stub(), + removeCallback: sandbox.stub(), + }; + const delegateMeter = new NoopMeter(); + sandbox + .stub(delegateMeter, 'createObservableGauge') + .returns(delegateObservable); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + const [result] = privateMeter._mapObservablesToDelegates([ + proxyObservable as unknown as Observable, + ]); + + assert.strictEqual(result, delegateObservable); + }); + + it('does not bind pending instruments when no delegate is present', () => { + const meter = provider.getMeter('test') as ProxyMeter; + const privateMeter = meter as unknown as { + _bindPendingInstruments: () => void; + _instruments: Set; + }; + const instrument = { + hasDelegate: sandbox.stub().returns(false), + bindDelegate: sandbox.stub(), + }; + privateMeter._instruments.add(instrument); + + privateMeter._bindPendingInstruments(); + + sandbox.assert.notCalled(instrument.bindDelegate); + assert.strictEqual(privateMeter._instruments.size, 1); + }); + + it('falls back to NOOP meter when no delegate is available', () => { + const meter = provider.getMeter('test') as ProxyMeter; + const privateMeter = meter as unknown as { _getMeter: () => Meter }; + + const result = privateMeter._getMeter(); + + assert.strictEqual(result, NOOP_METER); + }); + + it('allows proxy instruments to attempt delegate binding before delegation', () => { + const meter = provider.getMeter('test'); + const counter = meter.createCounter('lazy'); + + assert.doesNotThrow(() => + (counter as unknown as { bindDelegate: () => void }).bindDelegate() + ); + }); + + it('hydrates instruments lazily when pending state flush is prevented', () => { + const meter = provider.getMeter('test') as ProxyMeter; + const counter = meter.createCounter('lazy'); + const delegateCounter: Counter = { + add: sandbox.stub(), + } as Counter; + const delegateMeter = new NoopMeter(); + sandbox.stub(delegateMeter, 'createCounter').returns(delegateCounter); + sandbox.stub(meter as unknown as { _flushPendingState: () => void }, '_flushPendingState'); + + provider.setDelegate({ + getMeter() { + return delegateMeter; + }, + }); + + counter.add(3); + + sandbox.assert.calledOnce( + delegateMeter.createCounter as sinon.SinonStub + ); + sandbox.assert.calledOnce(delegateCounter.add as sinon.SinonStub); + }); }); }); From 59fc2fd4418832ba5a4aa09bfbf5cf543b6d7441 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:12:43 -0800 Subject: [PATCH 15/15] Lint --- .../proxy-implementations/proxy-meter.test.ts | 98 ++++++++++--------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/api/test/common/proxy-implementations/proxy-meter.test.ts b/api/test/common/proxy-implementations/proxy-meter.test.ts index 1de0a228686..a50d74b1085 100644 --- a/api/test/common/proxy-implementations/proxy-meter.test.ts +++ b/api/test/common/proxy-implementations/proxy-meter.test.ts @@ -92,7 +92,8 @@ describe('ProxyMeter', () => { it('creates observable counters that buffer callbacks before delegation', () => { const meter = provider.getMeter('test'); - const observableCounter = meter.createObservableCounter('observable-counter'); + const observableCounter = + meter.createObservableCounter('observable-counter'); const observableUpDownCounter = meter.createObservableUpDownCounter( 'observable-up-down-counter' ); @@ -100,7 +101,9 @@ describe('ProxyMeter', () => { const upDownCallback = sandbox.stub(); assert.doesNotThrow(() => observableCounter.addCallback(counterCallback)); - assert.doesNotThrow(() => observableCounter.removeCallback(counterCallback)); + assert.doesNotThrow(() => + observableCounter.removeCallback(counterCallback) + ); assert.doesNotThrow(() => observableUpDownCounter.addCallback(upDownCallback) ); @@ -333,7 +336,9 @@ describe('ProxyMeter', () => { add: addStub, } as UpDownCounter; const delegateMeter = new NoopMeter(); - sandbox.stub(delegateMeter, 'createUpDownCounter').returns(delegateUpDownCounter); + sandbox + .stub(delegateMeter, 'createUpDownCounter') + .returns(delegateUpDownCounter); provider.setDelegate({ getMeter() { @@ -347,7 +352,9 @@ describe('ProxyMeter', () => { it('hydrates observable counters that were created before delegation', () => { const meter = provider.getMeter('test'); - const observableCounter = meter.createObservableCounter('pre-observable-counter'); + const observableCounter = meter.createObservableCounter( + 'pre-observable-counter' + ); const callback = sandbox.stub(); observableCounter.addCallback(callback); @@ -458,36 +465,36 @@ describe('ProxyMeter', () => { assert.strictEqual(registeredObservables[0], delegateObservable); }); - it('remaps proxy observables when registering batch callbacks after delegation', () => { - const meter = provider.getMeter('test'); - const proxyObservable = meter.createObservableGauge('proxy-batch'); - const callback = sandbox.stub(); - 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; - }, - }); - - meter.addBatchObservableCallback(callback, [proxyObservable]); - - sandbox.assert.calledOnce(addBatchStub); - const [, registeredObservables] = addBatchStub.firstCall.args; - assert.strictEqual(registeredObservables[0], delegateObservable); + it('remaps proxy observables when registering batch callbacks after delegation', () => { + const meter = provider.getMeter('test'); + const proxyObservable = meter.createObservableGauge('proxy-batch'); + const callback = sandbox.stub(); + 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; + }, }); + meter.addBatchObservableCallback(callback, [proxyObservable]); + + sandbox.assert.calledOnce(addBatchStub); + const [, registeredObservables] = addBatchStub.firstCall.args; + assert.strictEqual(registeredObservables[0], delegateObservable); + }); + it('removes batch callbacks via the current delegate before delegation', () => { const meter = provider.getMeter('test'); const observable = meter.createObservableGauge('batch'); @@ -512,18 +519,18 @@ describe('ProxyMeter', () => { ); }); - it('removes batch callbacks by delegating to the noop meter when unset', () => { - const meter = provider.getMeter('test'); - const observable = meter.createObservableGauge('noop-batch'); - const callback = sandbox.stub(); - meter.addBatchObservableCallback(callback, [observable]); + it('removes batch callbacks by delegating to the noop meter when unset', () => { + const meter = provider.getMeter('test'); + const observable = meter.createObservableGauge('noop-batch'); + const callback = sandbox.stub(); + meter.addBatchObservableCallback(callback, [observable]); - const noopSpy = sandbox.spy(NOOP_METER, 'removeBatchObservableCallback'); + const noopSpy = sandbox.spy(NOOP_METER, 'removeBatchObservableCallback'); - meter.removeBatchObservableCallback(callback, [observable]); + meter.removeBatchObservableCallback(callback, [observable]); - sandbox.assert.calledOnce(noopSpy); - }); + sandbox.assert.calledOnce(noopSpy); + }); }); describe('proxy instrument internals', () => { @@ -631,7 +638,10 @@ describe('ProxyMeter', () => { } as Counter; const delegateMeter = new NoopMeter(); sandbox.stub(delegateMeter, 'createCounter').returns(delegateCounter); - sandbox.stub(meter as unknown as { _flushPendingState: () => void }, '_flushPendingState'); + sandbox.stub( + meter as unknown as { _flushPendingState: () => void }, + '_flushPendingState' + ); provider.setDelegate({ getMeter() { @@ -641,9 +651,7 @@ describe('ProxyMeter', () => { counter.add(3); - sandbox.assert.calledOnce( - delegateMeter.createCounter as sinon.SinonStub - ); + sandbox.assert.calledOnce(delegateMeter.createCounter as sinon.SinonStub); sandbox.assert.calledOnce(delegateCounter.add as sinon.SinonStub); }); });