diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 38ffb7d38b1..844b9d2f5ee 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to experimental packages in this project will be documented * allowing overrides of the `User-Agent` header was not specification compliant. * feat(exporter-*-otlp*)!: remove environment-variable specific code from browser exporters * (user-facing) removes the ability to configure browser exporters by using `process.env` polyfills +* feat(sdk-node)!: Automatically configure logs exporter [#4740](https://github.com/open-telemetry/opentelemetry-js/pull/4740) ### :rocket: (Enhancement) diff --git a/experimental/packages/opentelemetry-sdk-node/README.md b/experimental/packages/opentelemetry-sdk-node/README.md index 147d1725251..e2e5fe39d65 100644 --- a/experimental/packages/opentelemetry-sdk-node/README.md +++ b/experimental/packages/opentelemetry-sdk-node/README.md @@ -91,6 +91,14 @@ Use a custom context manager. Default: [AsyncHooksContextManager](../../../packa Use a custom propagator. Default: [CompositePropagator](../../../packages/opentelemetry-core/src/propagation/composite.ts) using [W3C Trace Context](../../../packages/opentelemetry-core/README.md#w3ctracecontextpropagator-propagator) and [Baggage](../../../packages/opentelemetry-core/README.md#baggage-propagator) +### logRecordProcessor + +Deprecated, please use [logRecordProcessors](#logrecordprocessors) instead. + +### logRecordProcessors + +An array of log record processors to register to the logger provider. + ### metricReader Add a [MetricReader](../opentelemetry-sdk-metrics/src/export/MetricReader.ts) @@ -177,15 +185,16 @@ Set the log level by setting the `OTEL_LOG_LEVEL` environment variable to enums: The default level is `INFO`. -## Configure Trace Exporter from environment +## Configure Exporters from environment -This is an alternative to programmatically configuring an exporter or span processor. This package will auto setup the default `otlp` exporter with `http/protobuf` protocol if `traceExporter` or `spanProcessor` hasn't been passed into the `NodeSDK` constructor. +This is an alternative to programmatically configuring an exporter or span processor. For traces this package will auto setup the default `otlp` exporter with `http/protobuf` protocol if `traceExporter` or `spanProcessor` hasn't been passed into the `NodeSDK` constructor. ### Exporters | Environment variable | Description | |----------------------|-------------| | OTEL_TRACES_EXPORTER | List of exporters to be used for tracing, separated by commas. Options include `otlp`, `jaeger`, `zipkin`, and `none`. Default is `otlp`. `none` means no autoconfigured exporter. | +| OTEL_LOGS_EXPORTER | List of exporters to be used for logging, separated by commas. Options include `otlp`, `console` and `none`. Default is `otlp`. `none` means no autoconfigured exporter. | ### OTLP Exporter @@ -194,6 +203,7 @@ This is an alternative to programmatically configuring an exporter or span proce | OTEL_EXPORTER_OTLP_PROTOCOL | The transport protocol to use on OTLP trace, metric, and log requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | | OTEL_EXPORTER_OTLP_TRACES_PROTOCOL | The transport protocol to use on OTLP trace requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | | OTEL_EXPORTER_OTLP_METRICS_PROTOCOL | The transport protocol to use on OTLP metric requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | +| OTEL_EXPORTER_OTLP_LOGS_PROTOCOL | The transport protocol to use on OTLP log requests. Options include `grpc`, `http/protobuf`, and `http/json`. Default is `http/protobuf`. | Additionally, you can specify other applicable environment variables that apply to each exporter such as the following: diff --git a/experimental/packages/opentelemetry-sdk-node/package.json b/experimental/packages/opentelemetry-sdk-node/package.json index f1452904d69..30f39491cba 100644 --- a/experimental/packages/opentelemetry-sdk-node/package.json +++ b/experimental/packages/opentelemetry-sdk-node/package.json @@ -50,6 +50,9 @@ "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", "@opentelemetry/exporter-trace-otlp-http": "0.52.1", "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.52.1", + "@opentelemetry/exporter-logs-otlp-http": "0.52.1", + "@opentelemetry/exporter-logs-otlp-proto": "0.52.1", "@opentelemetry/exporter-zipkin": "1.25.1", "@opentelemetry/instrumentation": "0.52.1", "@opentelemetry/resources": "1.25.1", diff --git a/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts b/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts index a4b9ee1286c..4db49699391 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/TracerProviderWithEnvExporter.ts @@ -32,6 +32,7 @@ import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/expo import { OTLPTraceExporter as OTLPHttpTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { filterBlanksAndNulls } from './utils'; export class TracerProviderWithEnvExporters extends NodeTracerProvider { private _configuredExporters: SpanExporter[] = []; @@ -94,7 +95,7 @@ export class TracerProviderWithEnvExporters extends NodeTracerProvider { public constructor(config: NodeTracerConfig = {}) { super(config); - let traceExportersList = this.filterBlanksAndNulls( + let traceExportersList = filterBlanksAndNulls( Array.from(new Set(getEnv().OTEL_TRACES_EXPORTER.split(','))) ); @@ -175,8 +176,4 @@ export class TracerProviderWithEnvExporters extends NodeTracerProvider { } }); } - - private filterBlanksAndNulls(list: string[]): string[] { - return list.map(item => item.trim()).filter(s => s !== 'null' && s !== ''); - } } diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index 24a7d2332d0..727be5d70e9 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -37,7 +37,17 @@ import { Resource, ResourceDetectionConfig, } from '@opentelemetry/resources'; -import { LogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'; +import { + LogRecordProcessor, + LoggerProvider, + BatchLogRecordProcessor, + ConsoleLogRecordExporter, + LogRecordExporter, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; +import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; +import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; import { MeterProvider, MetricReader, View } from '@opentelemetry/sdk-metrics'; import { BatchSpanProcessor, @@ -51,7 +61,7 @@ import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; import { NodeSDKConfiguration } from './types'; import { TracerProviderWithEnvExporters } from './TracerProviderWithEnvExporter'; import { getEnv, getEnvWithoutDefaults } from '@opentelemetry/core'; -import { getResourceDetectorsFromEnv } from './utils'; +import { getResourceDetectorsFromEnv, filterBlanksAndNulls } from './utils'; /** This class represents everything needed to register a fully configured OpenTelemetry Node.js SDK */ @@ -70,7 +80,7 @@ export type LoggerProviderConfig = { /** * Reference to the LoggerRecordProcessor instance by the NodeSDK */ - logRecordProcessor: LogRecordProcessor; + logRecordProcessors: LogRecordProcessor[]; }; export class NodeSDK { @@ -173,10 +183,19 @@ export class NodeSDK { }; } - if (configuration.logRecordProcessor) { + if (configuration.logRecordProcessors) { + this._loggerProviderConfig = { + logRecordProcessors: configuration.logRecordProcessors, + }; + } else if (configuration.logRecordProcessor) { this._loggerProviderConfig = { - logRecordProcessor: configuration.logRecordProcessor, + logRecordProcessors: [configuration.logRecordProcessor], }; + diag.warn( + "The 'logRecordProcessor' option is deprecated. Please use 'logRecordProcessors' instead." + ); + } else { + this.configureLoggerProviderFromEnv(); } if (configuration.metricReader || configuration.views) { @@ -257,9 +276,11 @@ export class NodeSDK { const loggerProvider = new LoggerProvider({ resource: this._resource, }); - loggerProvider.addLogRecordProcessor( - this._loggerProviderConfig.logRecordProcessor - ); + + for (const logRecordProcessor of this._loggerProviderConfig + .logRecordProcessors) { + loggerProvider.addLogRecordProcessor(logRecordProcessor); + } this._loggerProvider = loggerProvider; @@ -308,4 +329,71 @@ export class NodeSDK { .then(() => {}) ); } + + private configureLoggerProviderFromEnv(): void { + const logExportersList = process.env.OTEL_LOGS_EXPORTER ?? ''; + const enabledExporters = filterBlanksAndNulls(logExportersList.split(',')); + + if (enabledExporters.length === 0) { + diag.info('OTEL_LOGS_EXPORTER is empty. Using default otlp exporter.'); + enabledExporters.push('otlp'); + } + + if (enabledExporters.includes('none')) { + diag.info( + `OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.` + ); + return; + } + + const exporters: LogRecordExporter[] = []; + + enabledExporters.forEach(exporter => { + if (exporter === 'otlp') { + const protocol = ( + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? + process.env.OTEL_EXPORTER_OTLP_PROTOCOL + )?.trim(); + + switch (protocol) { + case 'grpc': + exporters.push(new OTLPGrpcLogExporter()); + break; + case 'http/json': + exporters.push(new OTLPHttpLogExporter()); + break; + case 'http/protobuf': + exporters.push(new OTLPProtoLogExporter()); + break; + case undefined: + case '': + exporters.push(new OTLPProtoLogExporter()); + break; + default: + diag.warn( + `Unsupported OTLP logs protocol: "${protocol}". Using http/protobuf.` + ); + exporters.push(new OTLPProtoLogExporter()); + } + } else if (exporter === 'console') { + exporters.push(new ConsoleLogRecordExporter()); + } else { + diag.warn( + `Unsupported OTEL_LOGS_EXPORTER value: "${exporter}". Supported values are: otlp, console, none.` + ); + } + }); + + if (exporters.length > 0) { + this._loggerProviderConfig = { + logRecordProcessors: exporters.map(exporter => { + if (exporter instanceof ConsoleLogRecordExporter) { + return new SimpleLogRecordProcessor(exporter); + } else { + return new BatchLogRecordProcessor(exporter); + } + }), + }; + } + } } diff --git a/experimental/packages/opentelemetry-sdk-node/src/types.ts b/experimental/packages/opentelemetry-sdk-node/src/types.ts index c3b2a1cd84c..91d2dc461cd 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/types.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/types.ts @@ -32,7 +32,9 @@ export interface NodeSDKConfiguration { autoDetectResources: boolean; contextManager: ContextManager; textMapPropagator: TextMapPropagator; + /** @deprecated use logRecordProcessors instead*/ logRecordProcessor: LogRecordProcessor; + logRecordProcessors?: LogRecordProcessor[]; metricReader: MetricReader; views: View[]; instrumentations: (Instrumentation | Instrumentation[])[]; diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index 4bc696f402b..bae8a028e65 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -61,3 +61,7 @@ export function getResourceDetectorsFromEnv(): Array { return resourceDetector || []; }); } + +export function filterBlanksAndNulls(list: string[]): string[] { + return list.map(item => item.trim()).filter(s => s !== 'null' && s !== ''); +} diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index f394dbc91a6..1d8bc87fbd4 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -71,7 +71,12 @@ import { SimpleLogRecordProcessor, InMemoryLogRecordExporter, LoggerProvider, + ConsoleLogRecordExporter, + BatchLogRecordProcessor, } from '@opentelemetry/sdk-logs'; +import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; +import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; import { SEMRESATTRS_HOST_NAME, SEMRESATTRS_PROCESS_PID, @@ -92,6 +97,7 @@ describe('Node SDK', () => { trace.disable(); propagation.disable(); metrics.disable(); + logs.disable(); ctxManager = context['_getContextManager'](); propagator = propagation['_getGlobalPropagator'](); @@ -129,7 +135,6 @@ describe('Node SDK', () => { 'tracer provider should not have changed' ); assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); - assert.ok(!(logs.getLoggerProvider() instanceof LoggerProvider)); delete env.OTEL_TRACES_EXPORTER; await sdk.shutdown(); }); @@ -327,6 +332,44 @@ describe('Node SDK', () => { delete env.OTEL_TRACES_EXPORTER; }); + it('should register a logger provider if multiple log record processors are provided', async () => { + const logRecordExporter = new InMemoryLogRecordExporter(); + const simpleLogRecordProcessor = new SimpleLogRecordProcessor( + logRecordExporter + ); + const batchLogRecordProcessor = new BatchLogRecordProcessor( + logRecordExporter + ); + const sdk = new NodeSDK({ + logRecordProcessors: [ + simpleLogRecordProcessor, + batchLogRecordProcessor, + ], + }); + + sdk.start(); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert(sharedState.registeredLogRecordProcessors.length === 2); + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + InMemoryLogRecordExporter + ); + assert( + sharedState.registeredLogRecordProcessors[0] instanceof + SimpleLogRecordProcessor + ); + assert( + sharedState.registeredLogRecordProcessors[1]._exporter instanceof + InMemoryLogRecordExporter + ); + assert( + sharedState.registeredLogRecordProcessors[1] instanceof + BatchLogRecordProcessor + ); + }); + it('should register a context manager if only a context manager is provided', async () => { // arrange const expectedContextManager = new AsyncHooksContextManager(); @@ -898,6 +941,144 @@ describe('Node SDK', () => { await sdk.shutdown(); }); }); + + describe('configuring logger provider from env', () => { + let stubLogger: Sinon.SinonStub; + + beforeEach(() => { + stubLogger = Sinon.stub(diag, 'info'); + }); + + afterEach(() => { + stubLogger.reset(); + delete env.OTEL_LOGS_EXPORTER; + delete env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL; + delete env.OTEL_EXPORTER_OTLP_PROTOCOL; + }); + + it('should not register the provider if OTEL_LOGS_EXPORTER contains none', async () => { + const logsAPIStub = Sinon.spy(logs, 'setGlobalLoggerProvider'); + env.OTEL_LOGS_EXPORTER = 'console,none'; + const sdk = new NodeSDK(); + sdk.start(); + assert.strictEqual( + stubLogger.args[0][0], + 'OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.' + ); + + Sinon.assert.notCalled(logsAPIStub); + await sdk.shutdown(); + }); + + it('should use otlp with http/protobuf by default', async () => { + const sdk = new NodeSDK(); + sdk.start(); + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPProtoLogExporter + ); + await sdk.shutdown(); + }); + + it('should set up all allowed exporters', async () => { + env.OTEL_LOGS_EXPORTER = 'console,otlp'; + const sdk = new NodeSDK(); + + sdk.start(); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert(sharedState.registeredLogRecordProcessors.length === 2); + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + ConsoleLogRecordExporter + ); + assert( + sharedState.registeredLogRecordProcessors[0] instanceof + SimpleLogRecordProcessor + ); + // defaults to http/protobuf + assert( + sharedState.registeredLogRecordProcessors[1]._exporter instanceof + OTLPProtoLogExporter + ); + assert( + sharedState.registeredLogRecordProcessors[1] instanceof + BatchLogRecordProcessor + ); + await sdk.shutdown(); + }); + + it('should use OTEL_EXPORTER_OTLP_LOGS_PROTOCOL for otlp protocol', async () => { + env.OTEL_LOGS_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'grpc'; + const sdk = new NodeSDK(); + + sdk.start(); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert(sharedState.registeredLogRecordProcessors.length === 1); + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPGrpcLogExporter + ); + await sdk.shutdown(); + }); + + it('should use OTLPHttpLogExporter when http/json is set', async () => { + env.OTEL_LOGS_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'http/json'; + const sdk = new NodeSDK(); + + sdk.start(); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert(sharedState.registeredLogRecordProcessors.length === 1); + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPHttpLogExporter + ); + await sdk.shutdown(); + }); + + it('should fall back to OTEL_EXPORTER_OTLP_PROTOCOL', async () => { + env.OTEL_LOGS_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_PROTOCOL = 'grpc'; + const sdk = new NodeSDK(); + + sdk.start(); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert(sharedState.registeredLogRecordProcessors.length === 1); + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPGrpcLogExporter + ); + await sdk.shutdown(); + }); + + it('should fall back to http/protobuf if invalid protocol is set', async () => { + env.OTEL_LOGS_EXPORTER = 'otlp'; + env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'grpc2'; + const sdk = new NodeSDK(); + + sdk.start(); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert(sharedState.registeredLogRecordProcessors.length === 1); + assert( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPProtoLogExporter + ); + await sdk.shutdown(); + }); + }); }); describe('setup exporter from env', () => { diff --git a/experimental/packages/opentelemetry-sdk-node/tsconfig.json b/experimental/packages/opentelemetry-sdk-node/tsconfig.json index 94c80b88469..f04c9fdb8e6 100644 --- a/experimental/packages/opentelemetry-sdk-node/tsconfig.json +++ b/experimental/packages/opentelemetry-sdk-node/tsconfig.json @@ -42,6 +42,15 @@ { "path": "../api-logs" }, + { + "path": "../exporter-logs-otlp-grpc" + }, + { + "path": "../exporter-logs-otlp-http" + }, + { + "path": "../exporter-logs-otlp-proto" + }, { "path": "../exporter-trace-otlp-grpc" },