Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions examples/multiple-metric-readers-README.md
Original file line number Diff line number Diff line change
@@ -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.
78 changes: 78 additions & 0 deletions examples/multiple-metric-readers.js
Original file line number Diff line number Diff line change
@@ -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');
14 changes: 13 additions & 1 deletion experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 17 additions & 7 deletions experimental/packages/opentelemetry-sdk-node/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. Fine because this package is experimental but make sure it is noted in the changelog.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to have anything to do with documenting or clarifying...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like #5777

/**
* List of {@link ViewOptions}s that should be passed to the MeterProvider
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions experimental/packages/opentelemetry-sdk-node/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
117 changes: 117 additions & 0 deletions experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,123 @@
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(

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (20.6.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (18)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (20)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (22)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (23)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (18.19.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-windows-tests

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (18.19.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (20.6.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (20)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / browser-tests

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (18)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (24)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / webworker-tests

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (22)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 399 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (23)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an import for sinon

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(

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (20.6.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (18)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (20)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (22)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (23)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / e2e-tests (18.19.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-windows-tests

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (18.19.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (20.6.0)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (20)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / browser-tests

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (18)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (24)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / webworker-tests

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (22)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.

Check failure on line 431 in experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts

View workflow job for this annotation

GitHub Actions / node-tests (23)

'sinon' refers to a UMD global, but the current file is a module. Consider adding an import instead.
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();
Expand Down
Loading
Loading