diff --git a/examples/multiple-metric-readers-README.md b/examples/multiple-metric-readers-README.md new file mode 100644 index 00000000000..afe9580ac7f --- /dev/null +++ b/examples/multiple-metric-readers-README.md @@ -0,0 +1,74 @@ +# Multiple Metric Readers Example + +This example demonstrates how to use the new `metricReaders` (plural) option in the NodeSDK configuration to register multiple metric readers simultaneously. + +## Features + +- **Multiple Metric Readers**: Configure multiple metric readers in a single SDK instance +- **Console Export**: Metrics are exported to the console for easy debugging +- **Prometheus Export**: Metrics are also exported to a Prometheus endpoint +- **Auto-instrumentation**: Automatic instrumentation of Node.js applications + +## Usage + +### Running the Example + +```bash +node multiple-metric-readers.js +``` + +### What Happens + +1. The SDK is configured with two metric readers: + - A `ConsoleMetricExporter` that prints metrics to the console + - A `PrometheusExporter` that exposes metrics on `http://localhost:9464/metrics` + +2. A counter metric is created and incremented every second + +3. Metrics are automatically exported to both destinations + +### API Changes + +This example demonstrates the new API that supports multiple metric readers: + +```javascript +// OLD (deprecated) - single metric reader +const sdk = new opentelemetry.NodeSDK({ + metricReader: singleMetricReader, // deprecated +}); + +// NEW - multiple metric readers +const sdk = new opentelemetry.NodeSDK({ + metricReaders: [consoleMetricReader, prometheusMetricReader], // new +}); +``` + +### Benefits + +- **Flexibility**: Export metrics to multiple destinations simultaneously +- **Debugging**: Console export for development and debugging +- **Production**: Prometheus export for production monitoring +- **Backward Compatibility**: The old `metricReader` option still works but shows a deprecation warning + +### Checking the Results + +1. **Console Output**: Watch the console for metric exports every second +2. **Prometheus Endpoint**: Visit `http://localhost:9464/metrics` to see the Prometheus-formatted metrics + +## Migration Guide + +If you're currently using the single `metricReader` option, you can migrate to the new `metricReaders` option: + +```javascript +// Before +const sdk = new opentelemetry.NodeSDK({ + metricReader: myMetricReader, +}); + +// After +const sdk = new opentelemetry.NodeSDK({ + metricReaders: [myMetricReader], +}); +``` + +The old `metricReader` option will continue to work but will show a deprecation warning. It's recommended to migrate to the new `metricReaders` option for future compatibility. \ No newline at end of file diff --git a/examples/multiple-metric-readers.js b/examples/multiple-metric-readers.js new file mode 100644 index 00000000000..5b52bc4b4cf --- /dev/null +++ b/examples/multiple-metric-readers.js @@ -0,0 +1,78 @@ +/* + * 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. + */ + +'use strict'; + +const process = require('process'); +const opentelemetry = require('@opentelemetry/sdk-node'); +const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); +const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base'); +const { resourceFromAttributes } = require('@opentelemetry/resources'); +const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions'); +const { + ConsoleMetricExporter, + PeriodicExportingMetricReader +} = require('@opentelemetry/sdk-metrics'); +const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); + +// Create multiple metric readers +const consoleMetricReader = new PeriodicExportingMetricReader({ + exporter: new ConsoleMetricExporter(), + exportIntervalMillis: 1000, + exportTimeoutMillis: 500, +}); + +const prometheusMetricReader = new PrometheusExporter({ + port: 9464, + endpoint: '/metrics', +}); + +// Configure the SDK with multiple metric readers +const sdk = new opentelemetry.NodeSDK({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'multiple-metric-readers-example', + }), + traceExporter: new ConsoleSpanExporter(), + // Use the new metricReaders option (plural) instead of the deprecated metricReader (singular) + metricReaders: [consoleMetricReader, prometheusMetricReader], + instrumentations: [getNodeAutoInstrumentations()] +}); + +// Initialize the SDK and register with the OpenTelemetry API +sdk.start(); + +// Create a meter and some metrics +const meter = opentelemetry.metrics.getMeter('example-meter'); +const counter = meter.createCounter('example_counter', { + description: 'An example counter', +}); + +// Increment the counter every second +setInterval(() => { + counter.add(1, { 'example.label': 'value' }); + console.log('Counter incremented'); +}, 1000); + +// Gracefully shut down the SDK on process exit +process.on('SIGTERM', () => { + sdk.shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); +}); + +console.log('Multiple metric readers example started'); +console.log('Metrics will be exported to console and Prometheus endpoint at http://localhost:9464/metrics'); \ No newline at end of file diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 56a9316c7e6..b680b2050d9 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -10,6 +10,18 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features +* feat(sdk-node): Add support for multiple metric readers via the new `metricReaders` option in NodeSDK configuration. Users can now register multiple metric readers (e.g., Console, Prometheus) directly through the NodeSDK constructor. The old `metricReader` (singular) option is now deprecated and will show a warning if used, but remains supported for backward compatibility. Comprehensive tests and documentation have been added. [#5760](https://github.com/open-telemetry/opentelemetry-js/issues/5760) + * **Migration:** + - Before: + ```js + const sdk = new NodeSDK({ metricReader: myMetricReader }); + ``` + - After: + ```js + const sdk = new NodeSDK({ metricReaders: [myMetricReader] }); + ``` + * Users should migrate to the new `metricReaders` array option for future compatibility. The old option will continue to work but is deprecated. + ### :bug: Bug Fixes ### :books: Documentation @@ -123,7 +135,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :house: (Internal) -* chore(instrumentation-grpc): remove unused findIndex() function [#5372](https://github.com/open-telemetry/opentelemetry-js/pull/5372) @cjihrig +* refactor(instrumentation-grpc): remove unused findIndex() function [#5372](https://github.com/open-telemetry/opentelemetry-js/pull/5372) @cjihrig * refactor(otlp-exporter-base): remove unnecessary isNaN() checks [#5374](https://github.com/open-telemetry/opentelemetry-js/pull/5374) @cjihrig * refactor(exporter-prometheus): remove unnecessary isNaN() check [#5377](https://github.com/open-telemetry/opentelemetry-js/pull/5377) @cjihrig * refactor(sdk-node): move code to auto-instantiate propagators into utils [#5355](https://github.com/open-telemetry/opentelemetry-js/pull/5355) @pichlermarc diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index e653b55dc15..8ff8e2d6c20 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -85,9 +85,9 @@ import { export type MeterProviderConfig = { /** - * Reference to the MetricReader instance by the NodeSDK + * Reference to the MetricReader instances by the NodeSDK */ - reader?: IMetricReader; + readers?: IMetricReader[]; /** * List of {@link ViewOptions}s that should be passed to the MeterProvider */ @@ -312,10 +312,20 @@ export class NodeSDK { this.configureLoggerProviderFromEnv(); } - if (configuration.metricReader || configuration.views) { + if ( + configuration.metricReaders || + configuration.metricReader || + configuration.views + ) { const meterProviderConfig: MeterProviderConfig = {}; - if (configuration.metricReader) { - meterProviderConfig.reader = configuration.metricReader; + + if (configuration.metricReaders) { + meterProviderConfig.readers = configuration.metricReaders; + } else if (configuration.metricReader) { + meterProviderConfig.readers = [configuration.metricReader]; + diag.warn( + "The 'metricReader' option is deprecated. Please use 'metricReaders' instead." + ); } if (configuration.views) { @@ -395,8 +405,8 @@ export class NodeSDK { configureMetricProviderFromEnv(); if (this._meterProviderConfig || metricReadersFromEnv.length > 0) { const readers: IMetricReader[] = []; - if (this._meterProviderConfig?.reader) { - readers.push(this._meterProviderConfig.reader); + if (this._meterProviderConfig?.readers) { + readers.push(...this._meterProviderConfig.readers); } if (readers.length === 0) { diff --git a/experimental/packages/opentelemetry-sdk-node/src/types.ts b/experimental/packages/opentelemetry-sdk-node/src/types.ts index ff400de8f21..48b363a18f9 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/types.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/types.ts @@ -35,7 +35,9 @@ export interface NodeSDKConfiguration { /** @deprecated use logRecordProcessors instead*/ logRecordProcessor: LogRecordProcessor; logRecordProcessors?: LogRecordProcessor[]; + /** @deprecated use metricReaders instead*/ metricReader: IMetricReader; + metricReaders?: IMetricReader[]; views: ViewOptions[]; instrumentations: (Instrumentation | Instrumentation[])[]; resource: Resource; diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 8884d4b7455..11fba7f5159 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -322,6 +322,123 @@ describe('Node SDK', () => { delete env.OTEL_TRACES_EXPORTER; }); + it('should register a meter provider if multiple readers are provided', async () => { + // need to set OTEL_TRACES_EXPORTER to none since default value is otlp + // which sets up an exporter and affects the context manager + env.OTEL_TRACES_EXPORTER = 'none'; + const consoleExporter = new ConsoleMetricExporter(); + const inMemoryExporter = new InMemoryMetricExporter( + AggregationTemporality.CUMULATIVE + ); + const metricReader1 = new PeriodicExportingMetricReader({ + exporter: consoleExporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100, + }); + const metricReader2 = new PeriodicExportingMetricReader({ + exporter: inMemoryExporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100, + }); + + const sdk = new NodeSDK({ + metricReaders: [metricReader1, metricReader2], + autoDetectResources: false, + }); + + sdk.start(); + + assert.strictEqual( + context['_getContextManager'](), + ctxManager, + 'context manager should not change' + ); + assert.strictEqual( + propagation['_getGlobalPropagator'](), + propagator, + 'propagator should not change' + ); + assert.strictEqual( + (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(), + delegate, + 'tracer provider should not have changed' + ); + + const meterProvider = metrics.getMeterProvider() as MeterProvider; + assert.ok(meterProvider instanceof MeterProvider); + + // Verify that both metric readers are registered + const sharedState = (meterProvider as any)['_sharedState']; + assert.strictEqual(sharedState.metricCollectors.length, 2); + + await sdk.shutdown(); + delete env.OTEL_TRACES_EXPORTER; + }); + + it('should show deprecation warning when using metricReader option', async () => { + // need to set OTEL_TRACES_EXPORTER to none since default value is otlp + // which sets up an exporter and affects the context manager + env.OTEL_TRACES_EXPORTER = 'none'; + const exporter = new ConsoleMetricExporter(); + const metricReader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100, + }); + + const warnSpy = Sinon.spy(diag, 'warn'); + + const sdk = new NodeSDK({ + metricReader: metricReader, + autoDetectResources: false, + }); + + sdk.start(); + + // Verify deprecation warning was shown + sinon.assert.calledWith( + warnSpy, + "The 'metricReader' option is deprecated. Please use 'metricReaders' instead." + ); + + assert.ok(metrics.getMeterProvider() instanceof MeterProvider); + + await sdk.shutdown(); + delete env.OTEL_TRACES_EXPORTER; + }); + + it('should not show deprecation warning when using metricReaders option', async () => { + // need to set OTEL_TRACES_EXPORTER to none since default value is otlp + // which sets up an exporter and affects the context manager + env.OTEL_TRACES_EXPORTER = 'none'; + const exporter = new ConsoleMetricExporter(); + const metricReader = new PeriodicExportingMetricReader({ + exporter: exporter, + exportIntervalMillis: 100, + exportTimeoutMillis: 100, + }); + + const warnSpy = Sinon.spy(diag, 'warn'); + + const sdk = new NodeSDK({ + metricReaders: [metricReader], + autoDetectResources: false, + }); + + sdk.start(); + + // Verify no metricReader deprecation warning was shown + sinon.assert.neverCalledWith( + warnSpy, + "The 'metricReader' option is deprecated. Please use 'metricReaders' instead." + ); + + assert.ok(metrics.getMeterProvider() instanceof MeterProvider); + + await sdk.shutdown(); + delete env.OTEL_TRACES_EXPORTER; + }); + it('should register a logger provider if a log record processor is provided', async () => { env.OTEL_TRACES_EXPORTER = 'none'; const logRecordExporter = new InMemoryLogRecordExporter(); diff --git a/test-backward-compatibility.js b/test-backward-compatibility.js new file mode 100644 index 00000000000..c18cca1b3fb --- /dev/null +++ b/test-backward-compatibility.js @@ -0,0 +1,43 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/sdk-node'); +const { metrics } = require('@opentelemetry/api'); +const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base'); +const { resourceFromAttributes } = require('@opentelemetry/resources'); +const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions'); +const { + ConsoleMetricExporter, + PeriodicExportingMetricReader +} = require('@opentelemetry/sdk-metrics'); + +// Test backward compatibility with the old metricReader option +const oldMetricReader = new PeriodicExportingMetricReader({ + exporter: new ConsoleMetricExporter(), + exportIntervalMillis: 1000, + exportTimeoutMillis: 500, +}); + +const sdk = new opentelemetry.NodeSDK({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'backward-compatibility-test', + }), + traceExporter: new ConsoleSpanExporter(), + // This should still work but show a deprecation warning + metricReader: oldMetricReader, +}); + +console.log('Testing backward compatibility with metricReader option...'); +sdk.start(); + +// Create a simple metric to verify it works +const meter = metrics.getMeter('test-meter'); +const counter = meter.createCounter('test_counter'); +counter.add(1); + +console.log('Backward compatibility test completed successfully!'); +console.log('You should see a deprecation warning above.'); + +sdk.shutdown().then(() => { + console.log('Test completed.'); + process.exit(0); +}); \ No newline at end of file