From 434a8b207d3c5ddb24514c570bc10000a9b5c6e8 Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 12 Feb 2026 10:19:59 -0500 Subject: [PATCH 1/8] add logger provider --- experimental/CHANGELOG.md | 1 + .../{test => src}/semconv.ts | 0 .../opentelemetry-sdk-node/src/start.ts | 128 +++- .../opentelemetry-sdk-node/src/types.ts | 9 +- .../opentelemetry-sdk-node/src/utils.ts | 142 ++++- .../test/fixtures/logger.yaml | 117 ++++ .../test/fixtures/resources.yaml | 40 ++ .../opentelemetry-sdk-node/test/sdk.test.ts | 2 +- .../opentelemetry-sdk-node/test/start.test.ts | 553 +++++++++++++++++- .../test/util/resource-assertions.ts | 2 +- 10 files changed, 970 insertions(+), 24 deletions(-) rename experimental/packages/opentelemetry-sdk-node/{test => src}/semconv.ts (100%) create mode 100644 experimental/packages/opentelemetry-sdk-node/test/fixtures/logger.yaml create mode 100644 experimental/packages/opentelemetry-sdk-node/test/fixtures/resources.yaml diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 72c86519839..81cd5677f71 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -18,6 +18,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm * feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telemetry/opentelemetry-js/pull/6304) @maryliag * feat(instrumentation): use the `internals: true` option with import-in-the-middle hook, allowing instrumentations to hook internal files in ES modules [#6344](https://github.com/open-telemetry/opentelemetry-js/pull/6344) @trentm +* feat(opentelemetry-sdk-node): set log provider for experimental start [#x](https://github.com/open-telemetry/opentelemetry-js/pull/x) @maryliag ### :bug: Bug Fixes diff --git a/experimental/packages/opentelemetry-sdk-node/test/semconv.ts b/experimental/packages/opentelemetry-sdk-node/src/semconv.ts similarity index 100% rename from experimental/packages/opentelemetry-sdk-node/test/semconv.ts rename to experimental/packages/opentelemetry-sdk-node/src/semconv.ts diff --git a/experimental/packages/opentelemetry-sdk-node/src/start.ts b/experimental/packages/opentelemetry-sdk-node/src/start.ts index f4cb368b42d..88071db20a0 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/start.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/start.ts @@ -15,16 +15,41 @@ */ import { ConfigFactory, + ConfigurationModel, createConfigFactory, } from '@opentelemetry/configuration'; -import { diag, DiagConsoleLogger } from '@opentelemetry/api'; import { + context, + diag, + DiagConsoleLogger, + propagation, +} from '@opentelemetry/api'; +import { + getInstanceID, + getLogRecordProcessorsFromConfiguration, getPropagatorFromConfiguration, - setupDefaultContextManager, - setupPropagator, + getResourceDetectorsFromConfiguration, + getResourceFromConfiguration, } from './utils'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; -import type { SDKOptions } from './types'; +import type { SDKComponents, SDKOptions } from './types'; +import { LoggerProvider } from '@opentelemetry/sdk-logs'; +import { logs } from '@opentelemetry/api-logs'; +import { + defaultResource, + detectResources, + Resource, + ResourceDetectionConfig, + ResourceDetector, + resourceFromAttributes, +} from '@opentelemetry/resources'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { ATTR_SERVICE_INSTANCE_ID } from './semconv'; +import { + CompositePropagator, + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from '@opentelemetry/core'; /** * @experimental Function to start the OpenTelemetry Node SDK @@ -47,16 +72,21 @@ export function startNodeSDK(sdkOptions: SDKOptions): { registerInstrumentations({ instrumentations: sdkOptions?.instrumentations?.flat() ?? [], }); - setupDefaultContextManager(); - setupPropagator( - sdkOptions?.textMapPropagator === null - ? null // null means don't set. - : (sdkOptions?.textMapPropagator ?? - getPropagatorFromConfiguration(config)) - ); + + const components = create(config, sdkOptions); + context.setGlobalContextManager(components.contextManager); + if (components.loggerProvider) { + logs.setGlobalLoggerProvider(components.loggerProvider); + } + if (components.propagator) { + propagation.setGlobalPropagator(components.propagator); + } const shutdownFn = async () => { const promises: Promise[] = []; + if (components.loggerProvider) { + promises.push(components.loggerProvider.shutdown()); + } await Promise.all(promises); }; return { shutdown: shutdownFn }; @@ -64,3 +94,79 @@ export function startNodeSDK(sdkOptions: SDKOptions): { const NOOP_SDK = { shutdown: async () => {}, }; + +/** + * Interpret configuration model and return SDK components. + */ +function create( + config: ConfigurationModel, + sdkOptions: SDKOptions +): SDKComponents { + const defaultContextManager = new AsyncLocalStorageContextManager(); + defaultContextManager.enable(); + const components: SDKComponents = { + contextManager: defaultContextManager, + }; + const resource = setupResource(config, sdkOptions); + + const propagator = + sdkOptions?.textMapPropagator === null + ? null // null means don't set. + : (sdkOptions?.textMapPropagator ?? + getPropagatorFromConfiguration(config)); + if (propagator) { + components.propagator = propagator; + } else if (propagator === undefined) { + components.propagator = new CompositePropagator({ + propagators: [ + new W3CTraceContextPropagator(), + new W3CBaggagePropagator(), + ], + }); + } + + const logProcessors = getLogRecordProcessorsFromConfiguration(config); + if (logProcessors) { + const loggerProvider = new LoggerProvider({ + resource: resource, + processors: logProcessors, + }); + components.loggerProvider = loggerProvider; + } + + return components; +} + +export function setupResource( + config: ConfigurationModel, + sdkOptions: SDKOptions +): Resource { + let resource: Resource = + getResourceFromConfiguration(config) ?? defaultResource(); + let resourceDetectors: ResourceDetector[] = []; + + if (sdkOptions.resourceDetectors != null) { + resourceDetectors = sdkOptions.resourceDetectors; + } else if (config.node_resource_detectors) { + resourceDetectors = getResourceDetectorsFromConfiguration(config); + } + + if (resourceDetectors.length > 0) { + const internalConfig: ResourceDetectionConfig = { + detectors: resourceDetectors, + }; + resource = resource.merge(detectResources(internalConfig)); + } + + const instanceId = getInstanceID(config); + resource = + instanceId === undefined + ? resource + : resource.merge( + resourceFromAttributes({ + [ATTR_SERVICE_INSTANCE_ID]: instanceId, + }) + ); + + return resource; +} diff --git a/experimental/packages/opentelemetry-sdk-node/src/types.ts b/experimental/packages/opentelemetry-sdk-node/src/types.ts index 8afe1a2ca08..19ef7158187 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/types.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/types.ts @@ -18,7 +18,7 @@ import type { ContextManager } from '@opentelemetry/api'; import { TextMapPropagator } from '@opentelemetry/api'; import { Instrumentation } from '@opentelemetry/instrumentation'; import { Resource, ResourceDetector } from '@opentelemetry/resources'; -import { LogRecordProcessor } from '@opentelemetry/sdk-logs'; +import { LoggerProvider, LogRecordProcessor } from '@opentelemetry/sdk-logs'; import { IMetricReader, ViewOptions } from '@opentelemetry/sdk-metrics'; import { Sampler, @@ -56,5 +56,12 @@ export interface NodeSDKConfiguration { */ export interface SDKOptions { instrumentations?: (Instrumentation | Instrumentation[])[]; + resourceDetectors?: ResourceDetector[]; textMapPropagator?: TextMapPropagator | null; } + +export interface SDKComponents { + contextManager: ContextManager; + loggerProvider?: LoggerProvider; + propagator?: TextMapPropagator; +} diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index abeecf71c98..c93f8d1b1a4 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -34,11 +34,14 @@ import { OTLPTraceExporter as OTLPHttpTraceExporter } from '@opentelemetry/expor import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; import { + DetectedResourceAttributes, envDetector, hostDetector, osDetector, processDetector, + Resource, ResourceDetector, + resourceFromAttributes, serviceInstanceIdDetector, } from '@opentelemetry/resources'; import { @@ -51,7 +54,14 @@ import { import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3'; import { JaegerPropagator } from '@opentelemetry/propagator-jaeger'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; -import { ConfigurationModel } from '@opentelemetry/configuration'; +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 { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; +import { + ConfigurationModel, + LogRecordExporterModel, +} from '@opentelemetry/configuration'; import { IMetricReader, PeriodicExportingMetricReader, @@ -63,8 +73,11 @@ import { OTLPMetricExporter as OTLPProtoMetricExporter } from '@opentelemetry/ex import { BatchLogRecordProcessor, BufferConfig, + ConsoleLogRecordExporter, LogRecordExporter, LoggerProviderConfig, + LogRecordProcessor, + SimpleLogRecordProcessor, } from '@opentelemetry/sdk-logs'; const RESOURCE_DETECTOR_ENVIRONMENT = 'env'; @@ -73,7 +86,24 @@ const RESOURCE_DETECTOR_OS = 'os'; const RESOURCE_DETECTOR_PROCESS = 'process'; const RESOURCE_DETECTOR_SERVICE_INSTANCE_ID = 'serviceinstance'; +export function getResourceFromConfiguration( + config: ConfigurationModel +): Resource | undefined { + if (config.resource && config.resource.attributes) { + const attr: DetectedResourceAttributes = {}; + for (let i = 0; i < config.resource.attributes.length; i++) { + const a = config.resource.attributes[i]; + attr[a.name] = a.value; + } + return resourceFromAttributes(attr, { + schemaUrl: config.resource.schema_url, + }); + } + return undefined; +} + export function getResourceDetectorsFromEnv(): Array { + console.log("AAAAAAAAA"); // When updating this list, make sure to also update the section `resourceDetectors` on README. const resourceDetectors = new Map([ [RESOURCE_DETECTOR_HOST, hostDetector], @@ -106,6 +136,37 @@ export function getResourceDetectorsFromEnv(): Array { }); } +export function getResourceDetectorsFromConfiguration( + config: ConfigurationModel +): Array { + // When updating this list, make sure to also update the section `resourceDetectors` on README. + const resourceDetectors = new Map([ + [RESOURCE_DETECTOR_HOST, hostDetector], + [RESOURCE_DETECTOR_OS, osDetector], + [RESOURCE_DETECTOR_SERVICE_INSTANCE_ID, serviceInstanceIdDetector], + [RESOURCE_DETECTOR_PROCESS, processDetector], + [RESOURCE_DETECTOR_ENVIRONMENT, envDetector], + ]); + + const resourceDetectorsFromConfig = config.node_resource_detectors ?? []; + + if (resourceDetectorsFromConfig.includes('all')) { + return [...resourceDetectors.values()].flat(); + } + + if (resourceDetectorsFromConfig.includes('none')) { + return []; + } + + return resourceDetectorsFromConfig.flatMap(detector => { + const resourceDetector = resourceDetectors.get(detector); + if (!resourceDetector) { + diag.warn(`Invalid resource detector "${detector}" specified`); + } + return resourceDetector || []; + }); +} + export function getOtlpProtocolFromEnv(): string { return ( getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? @@ -325,12 +386,6 @@ export function setupContextManager( context.setGlobalContextManager(contextManager); } -export function setupDefaultContextManager() { - const defaultContextManager = new AsyncLocalStorageContextManager(); - defaultContextManager.enable(); - context.setGlobalContextManager(defaultContextManager); -} - export function setupPropagator( propagator: TextMapPropagator | null | undefined ) { @@ -494,3 +549,76 @@ export function getBatchLogRecordProcessorFromEnv( getBatchLogRecordProcessorConfigFromEnv() ); } + +export function getLogRecordExporter( + exporter: LogRecordExporterModel +): LogRecordExporter { + if (exporter.otlp_http) { + const encoding = exporter.otlp_http.encoding; + if (encoding === 'json') { + return new OTLPHttpLogExporter({ + compression: + exporter.otlp_http.compression === 'gzip' + ? CompressionAlgorithm.GZIP + : CompressionAlgorithm.NONE, + }); + } + if (encoding === 'protobuf') { + return new OTLPProtoLogExporter(); + } + diag.warn( + `Unsupported OTLP logs encoding: ${encoding}. Using http/protobuf.` + ); + return new OTLPProtoLogExporter(); + } else if (exporter.otlp_grpc) { + return new OTLPGrpcLogExporter(); + } else if (exporter.console) { + return new ConsoleLogRecordExporter(); + } + diag.warn(`Unsupported Exporter value. Using OTLP http/protobuf.`); + return new OTLPProtoLogExporter(); +} + +export function getLogRecordProcessorsFromConfiguration( + config: ConfigurationModel +): LogRecordProcessor[] | undefined { + const logRecordProcessors: LogRecordProcessor[] = []; + config.logger_provider?.processors?.forEach(processor => { + if (processor.batch) { + logRecordProcessors.push( + new BatchLogRecordProcessor( + getLogRecordExporter(processor.batch.exporter), + { + maxQueueSize: processor.batch.max_queue_size, + maxExportBatchSize: processor.batch.max_export_batch_size, + scheduledDelayMillis: processor.batch.schedule_delay, + exportTimeoutMillis: processor.batch.export_timeout, + } + ) + ); + } + if (processor.simple) { + logRecordProcessors.push( + new SimpleLogRecordProcessor( + getLogRecordExporter(processor.simple.exporter) + ) + ); + } + }); + if (logRecordProcessors.length > 0) { + return logRecordProcessors; + } + return undefined; +} + +export function getInstanceID(config: ConfigurationModel): string | undefined { + if (config.resource?.attributes) { + for (let i = 0; i < config.resource.attributes.length; i++) { + const element = config.resource.attributes[i]; + if (element.name === 'service.instance.id') { + return element.value?.toString(); + } + } + } + return undefined; +} diff --git a/experimental/packages/opentelemetry-sdk-node/test/fixtures/logger.yaml b/experimental/packages/opentelemetry-sdk-node/test/fixtures/logger.yaml new file mode 100644 index 00000000000..db7f264c929 --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-node/test/fixtures/logger.yaml @@ -0,0 +1,117 @@ +file_format: "1.0-rc.3" +disabled: false +logger_provider: + # Configure log record processors. + processors: + - # Configure a batch log record processor. + batch: + # Configure delay interval (in milliseconds) between two consecutive exports. + # Value must be non-negative. + # If omitted or null, 1000 is used. + schedule_delay: 5000 + # Configure maximum allowed time (in milliseconds) to export data. + # Value must be non-negative. A value of 0 indicates no limit (infinity). + # If omitted or null, 30000 is used. + export_timeout: 30000 + # Configure maximum queue size. Value must be positive. + # If omitted or null, 2048 is used. + max_queue_size: 2048 + # Configure maximum batch size. Value must be positive. + # If omitted or null, 512 is used. + max_export_batch_size: 512 + # Configure exporter. + exporter: + # Configure exporter to be OTLP with HTTP transport. + otlp_http: + endpoint: http://localhost:4318/v1/logs + # Configure certificate used to verify a server's TLS credentials. + # Absolute path to certificate file in PEM format. + # If omitted or null, system default certificate verification is used for secure connections. + certificate_file: /app/cert.pem + # Configure mTLS private client key. + # Absolute path to client key file in PEM format. If set, .client_certificate must also be set. + # If omitted or null, mTLS is not used. + client_key_file: /app/cert.pem + # Configure mTLS client certificate. + # Absolute path to client certificate file in PEM format. If set, .client_key must also be set. + # If omitted or null, mTLS is not used. + client_certificate_file: /app/cert.pem + # Configure headers. Entries have higher priority than entries from .headers_list. + # If an entry's .value is null, the entry is ignored. + headers: + - name: api-key + value: "1234" + # Configure headers. Entries have lower priority than entries from .headers. + # The value is a list of comma separated key-value pairs matching the format of OTEL_EXPORTER_OTLP_HEADERS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options for details. + # If omitted or null, no headers are added. + headers_list: "api-key=1234" + # Configure compression. + # Values include: gzip, none. Implementations may support other compression algorithms. + # If omitted or null, none is used. + compression: gzip + # Configure max time (in milliseconds) to wait for each export. + # Value must be non-negative. A value of 0 indicates no limit (infinity). + # If omitted or null, 10000 is used. + timeout: 10000 + # Configure the encoding used for messages. + # Values include: protobuf, json. Implementations may not support json. + # If omitted or null, protobuf is used. + encoding: protobuf + - # Configure a batch log record processor. + batch: + # Configure exporter. + exporter: + # Configure exporter to be OTLP with gRPC transport. + otlp_grpc: + # Configure endpoint. + # If omitted or null, http://localhost:4317 is used. + endpoint: http://localhost:4317 + # Configure certificate used to verify a server's TLS credentials. + # Absolute path to certificate file in PEM format. + # If omitted or null, system default certificate verification is used for secure connections. + certificate_file: /app/cert.pem + # Configure mTLS private client key. + # Absolute path to client key file in PEM format. If set, .client_certificate must also be set. + # If omitted or null, mTLS is not used. + client_key_file: /app/cert.pem + # Configure mTLS client certificate. + # Absolute path to client certificate file in PEM format. If set, .client_key must also be set. + # If omitted or null, mTLS is not used. + client_certificate_file: /app/cert.pem + # Configure headers. Entries have higher priority than entries from .headers_list. + # If an entry's .value is null, the entry is ignored. + headers: + - name: api-key + value: "1234" + # Configure headers. Entries have lower priority than entries from .headers. + # The value is a list of comma separated key-value pairs matching the format of OTEL_EXPORTER_OTLP_HEADERS. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options for details. + # If omitted or null, no headers are added. + headers_list: "api-key=1234" + # Configure compression. + # Values include: gzip, none. Implementations may support other compression algorithms. + # If omitted or null, none is used. + compression: gzip + # Configure max time (in milliseconds) to wait for each export. + # Value must be non-negative. A value of 0 indicates no limit (infinity). + # If omitted or null, 10000 is used. + timeout: 10000 + # Configure client transport security for the exporter's connection. + # Only applicable when .endpoint is provided without http or https scheme. Implementations may choose to ignore .insecure. + # If omitted or null, false is used. + insecure: false + - # Configure a simple log record processor. + simple: + # Configure exporter. + exporter: + # Configure exporter to be console. + console: + # Configure log record limits. See also attribute_limits. + limits: + # Configure max attribute value size. Overrides .attribute_limits.attribute_value_length_limit. + # Value must be non-negative. + # If omitted or null, there is no limit. + attribute_value_length_limit: 4096 + # Configure max attribute count. Overrides .attribute_limits.attribute_count_limit. + # Value must be non-negative. + # If omitted or null, 128 is used. + attribute_count_limit: 128 \ No newline at end of file diff --git a/experimental/packages/opentelemetry-sdk-node/test/fixtures/resources.yaml b/experimental/packages/opentelemetry-sdk-node/test/fixtures/resources.yaml new file mode 100644 index 00000000000..76e92978ffb --- /dev/null +++ b/experimental/packages/opentelemetry-sdk-node/test/fixtures/resources.yaml @@ -0,0 +1,40 @@ +file_format: "1.0-rc.3" +disabled: false +resource: + # Configure resource attributes. Entries have higher priority than entries from .resource.attributes_list. + # Entries must contain .name and .value, and may optionally include .type. If an entry's .type omitted or null, string is used. + # The .value's type must match the .type. Values for .type include: string, bool, int, double, string_array, bool_array, int_array, double_array. + attributes: + - name: service.name + value: config-name + - name: string_key + value: value + type: string + - name: bool_key + value: true + type: bool + - name: int_key + value: 1 + type: int + - name: double_key + value: 1.1 + type: double + - name: string_array_key + value: [ "value1", "value2" ] + type: string_array + - name: bool_array_key + value: [ true, false ] + type: bool_array + - name: int_array_key + value: [ 1, 2 ] + type: int_array + - name: double_array_key + value: [ 1.1, 2.2 ] + type: double_array + # Configure resource attributes. Entries have lower priority than entries from .resource.attributes. + # The value is a list of comma separated key-value pairs matching the format of OTEL_RESOURCE_ATTRIBUTES. See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#general-sdk-configuration for details. + # If omitted or null, no resource attributes are added. + attributes_list: "service.namespace=config-namespace,service.version=1.0.0" + # Configure resource schema URL. + # If omitted or null, no schema URL is used. + schema_url: https://opentelemetry.io/schemas/1.16.0 \ No newline at end of file diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index dd4a23c44d0..192cd6d0938 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -80,7 +80,7 @@ import { OTLPTraceExporter as OTLPProtoTraceExporter } from '@opentelemetry/expo import { OTLPTraceExporter as OTLPGrpcTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; -import { ATTR_HOST_NAME, ATTR_PROCESS_PID } from './semconv'; +import { ATTR_HOST_NAME, ATTR_PROCESS_PID } from '../src/semconv'; function assertDefaultContextManagerRegistered() { assert.ok( diff --git a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts index 6d11b009dd5..2822287c2e9 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts @@ -15,12 +15,60 @@ */ import * as assert from 'assert'; -import { startNodeSDK } from '../src/start'; -import { context, diag, propagation } from '@opentelemetry/api'; -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { setupResource, startNodeSDK } from '../src/start'; import * as Sinon from 'sinon'; +import { + context, + propagation, + trace, + diag, + DiagLogLevel, + metrics, + DiagConsoleLogger, +} from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { W3CTraceContextPropagator } from '@opentelemetry/core'; +import { + assertServiceInstanceIdIsUUID, + assertServiceResource, +} from './util/resource-assertions'; +import { + envDetector, + processDetector, + hostDetector, + serviceInstanceIdDetector, + DetectedResource, +} from '@opentelemetry/resources'; +import { logs } from '@opentelemetry/api-logs'; +import { + SimpleLogRecordProcessor, + ConsoleLogRecordExporter, + BatchLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; +import { + ConfigFactory, + createConfigFactory, +} from '@opentelemetry/configuration'; +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 { ATTR_HOST_NAME, ATTR_PROCESS_PID } from '../src/semconv'; describe('startNodeSDK', function () { + let setGlobalLoggerProviderSpy: Sinon.SinonSpy; + + beforeEach(() => { + diag.disable(); + context.disable(); + trace.disable(); + propagation.disable(); + metrics.disable(); + logs.disable(); + + setGlobalLoggerProviderSpy = Sinon.spy(logs, 'setGlobalLoggerProvider'); + }); + const _origEnvVariables = { ...process.env }; afterEach(function () { @@ -38,6 +86,101 @@ describe('startNodeSDK', function () { Sinon.restore(); }); + describe('Basic Registration', () => { + it('should not register more than the minimal SDK components', async () => { + // need to set these to none, since the default value is 'otlp' + process.env.OTEL_TRACES_EXPORTER = 'none'; + process.env.OTEL_LOGS_EXPORTER = 'none'; + process.env.OTEL_METRICS_EXPORTER = 'none'; + const sdk = startNodeSDK({}); + + // These are minimal OTel functionality and always registered. + assertDefaultContextManagerRegistered(); + assertDefaultPropagatorRegistered(); + + assert.ok( + setGlobalLoggerProviderSpy.called === false, + 'logger provider should not have changed' + ); + + await sdk.shutdown(); + }); + + it('should register a diag logger with OTEL_LOG_LEVEL', () => { + process.env.OTEL_LOG_LEVEL = 'ERROR'; + + const spy = Sinon.spy(diag, 'setLogger'); + const sdk = startNodeSDK({}); + + assert.strictEqual(spy.callCount, 1); + assert.ok(spy.args[0][0] instanceof DiagConsoleLogger); + assert.deepStrictEqual(spy.args[0][1], { + logLevel: DiagLogLevel.ERROR, + }); + + sdk.shutdown(); + }); + + it('should register a diag logger with INFO with OTEL_LOG_LEVEL unset', () => { + delete process.env.OTEL_LOG_LEVEL; + + const spy = Sinon.spy(diag, 'setLogger'); + const sdk = startNodeSDK({}); + + assert.strictEqual(spy.callCount, 1); + assert.ok(spy.args[0][0] instanceof DiagConsoleLogger); + assert.deepStrictEqual(spy.args[0][1], { + logLevel: DiagLogLevel.INFO, + }); + sdk.shutdown(); + }); + + it('should register a propagator if only a propagator is provided', async () => { + const expectedPropagator = new W3CTraceContextPropagator(); + const sdk = startNodeSDK({ textMapPropagator: expectedPropagator }); + + const actualPropagator = propagation['_getGlobalPropagator'](); + assert.equal(actualPropagator, expectedPropagator); + await sdk.shutdown(); + }); + + it('should register propagators as defined in OTEL_PROPAGATORS', async () => { + process.env.OTEL_PROPAGATORS = 'b3'; + const sdk = startNodeSDK({}); + + assert.deepStrictEqual(propagation.fields(), ['b3']); + + await sdk.shutdown(); + }); + + it('should not register propagators OTEL_PROPAGATORS contains "none"', async () => { + process.env.OTEL_PROPAGATORS = 'none'; + const sdk = startNodeSDK({}); + + assert.deepStrictEqual(propagation.fields(), []); + + await sdk.shutdown(); + }); + + it('should not register propagators OTEL_PROPAGATORS contains "none" alongside valid propagator', async () => { + process.env.OTEL_PROPAGATORS = 'b3, none'; + const sdk = startNodeSDK({}); + + assert.deepStrictEqual(propagation.fields(), []); + + await sdk.shutdown(); + }); + + it('should not register propagators OTEL_PROPAGATORS contains valid propagator but option is set to null', async () => { + process.env.OTEL_PROPAGATORS = 'b3'; + const sdk = startNodeSDK({ textMapPropagator: null }); + + assert.deepStrictEqual(propagation.fields(), []); + + await sdk.shutdown(); + }); + }); + it('should return NOOP_SDK when disabled is true', () => { const info = Sinon.spy(diag, 'info'); process.env.OTEL_SDK_DISABLED = 'true'; @@ -57,6 +200,402 @@ describe('startNodeSDK', function () { sdk.shutdown(); }); + + it('should register a diag logger as info as default', () => { + const spy = Sinon.spy(diag, 'setLogger'); + const sdk = startNodeSDK({}); + + assert.strictEqual(spy.callCount, 1); + assert.ok(spy.args[0][0] instanceof DiagConsoleLogger); + assert.deepStrictEqual(spy.args[0][1], { + logLevel: DiagLogLevel.INFO, + }); + + sdk.shutdown(); + }); + + it('should register a logger provider if multiple log record processors are provided', async () => { + process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = 'test/fixtures/logger.yaml'; + const sdk = startNodeSDK({}); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert.ok(sharedState.registeredLogRecordProcessors.length === 3); + assert.ok( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPProtoLogExporter + ); + assert.ok( + sharedState.registeredLogRecordProcessors[0] instanceof + BatchLogRecordProcessor + ); + assert.ok( + sharedState.registeredLogRecordProcessors[1]._exporter instanceof + OTLPGrpcLogExporter + ); + assert.ok( + sharedState.registeredLogRecordProcessors[1] instanceof + BatchLogRecordProcessor + ); + assert.ok( + sharedState.registeredLogRecordProcessors[2]._exporter instanceof + ConsoleLogRecordExporter + ); + assert.ok( + sharedState.registeredLogRecordProcessors[2] instanceof + SimpleLogRecordProcessor + ); + await sdk.shutdown(); + }); + + describe('setupResources', async () => { + beforeEach(() => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; + }); + + afterEach(() => { + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + }); + + // Local function to test if a mocked method is ever called with a specific argument or regex matching for an argument. + // Needed because of race condition with parallel detectors. + const callArgsMatches = ( + mockedFunction: Sinon.SinonSpy, + regex: RegExp + ): boolean => { + return mockedFunction.getCalls().some(call => { + return call.args.some(callArgs => regex.test(callArgs.toString())); + }); + }; + + it('returns a merged resource with custom resource', async () => { + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, { + resourceDetectors: [ + processDetector, + { + detect(): DetectedResource { + return { + attributes: { customAttr: 'someValue' }, + }; + }, + }, + envDetector, + hostDetector, + ], + }); + await resource.waitForAsyncAttributes?.(); + + assert.strictEqual(resource.attributes['customAttr'], 'someValue'); + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + }); + + it('default detectors populate values properly', async () => { + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + + assert.equal(resource.attributes[ATTR_PROCESS_PID], undefined); + assert.equal(resource.attributes[ATTR_HOST_NAME], undefined); + }); + + it('no resource detectors with OTEL_NODE_RESOURCE_DETECTORS as none', async () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'none'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assert.equal(resource.attributes[ATTR_PROCESS_PID], undefined); + assert.equal(resource.attributes[ATTR_HOST_NAME], undefined); + }); + + it('should configure resources from config file', async () => { + process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = + 'test/fixtures/resources.yaml'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assert.deepStrictEqual( + resource.schemaUrl, + 'https://opentelemetry.io/schemas/1.16.0' + ); + + assert.deepStrictEqual(resource.attributes, { + 'service.name': 'config-name', + 'service.namespace': 'config-namespace', + 'service.version': '1.0.0', + bool_array_key: [true, false], + bool_key: true, + double_array_key: [1.1, 2.2], + double_key: 1.1, + int_array_key: [1, 2], + int_key: 1, + string_array_key: ['value1', 'value2'], + string_key: 'value', + }); + }); + + it('returns a merged resource with a buggy detector', async () => { + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, { + resourceDetectors: [ + processDetector, + { + detect() { + throw new Error('Buggy detector'); + }, + }, + envDetector, + hostDetector, + ], + }); + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + instanceId: '627cc493', + name: 'my-service', + namespace: 'default', + version: '0.0.1', + }); + }); + + // 1. If not auto-detecting resources, then NodeSDK should not + // complain about `OTEL_NODE_RESOURCE_DETECTORS` values. + // 2. If given resourceDetectors, then NodeSDK should not complain + // about `OTEL_NODE_RESOURCE_DETECTORS` values. + // + // Practically, these tests help ensure that there is no spurious + // diag error message when using OTEL_NODE_RESOURCE_DETECTORS with + // @opentelemetry/auto-instrumentations-node, which supports more values + // than this package (e.g. 'gcp'). + it('does not diag.warn when not using the envvar', async () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'env,os,no-such-detector'; + const diagMocks = { + error: Sinon.fake(), + warn: Sinon.fake(), + info: Sinon.fake(), + debug: Sinon.fake(), + verbose: Sinon.fake(), + }; + diag.setLogger(diagMocks, DiagLogLevel.DEBUG); + const sdk1 = startNodeSDK({}); + await sdk1.shutdown(); + + const sdk2 = startNodeSDK({ resourceDetectors: [envDetector] }); + await sdk2.shutdown(); + + assert.ok( + !callArgsMatches(diagMocks.error, /no-such-detector/), + 'diag.error() messages do not mention "no-such-detector"' + ); + }); + }); + + describe('configureServiceName', async () => { + it('should configure service name via OTEL_SERVICE_NAME env var', async () => { + process.env.OTEL_SERVICE_NAME = 'env-set-name'; + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.instance.id=my-instance-id'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + name: 'env-set-name', + instanceId: 'my-instance-id', + }); + }); + + it('should configure service name via OTEL_RESOURCE_ATTRIBUTES env var', async () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.name=resource-env-set-name,service.instance.id=my-instance-id'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + name: 'resource-env-set-name', + instanceId: 'my-instance-id', + }); + }); + }); + + describe('configureServiceInstanceId', async () => { + it('should configure service instance id via OTEL_RESOURCE_ATTRIBUTES env var', async () => { + process.env.OTEL_RESOURCE_ATTRIBUTES = + 'service.instance.id=627cc493,service.name=my-service,service.namespace'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assertServiceResource(resource, { + name: 'my-service', + instanceId: '627cc493', + }); + }); + + it('should configure service instance id via OTEL_NODE_RESOURCE_DETECTORS env var', async () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'env,host,os,serviceinstance'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assertServiceInstanceIdIsUUID(resource); + }); + + it('should configure service instance id with random UUID', async () => { + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, { + resourceDetectors: [ + processDetector, + envDetector, + hostDetector, + serviceInstanceIdDetector, + ], + }); + await resource.waitForAsyncAttributes?.(); + + assertServiceInstanceIdIsUUID(resource); + }); + }); + + describe('configuring logger provider from env', () => { + let stubLogger: Sinon.SinonStub; + + beforeEach(() => { + stubLogger = Sinon.stub(diag, 'info'); + }); + + afterEach(() => { + stubLogger.reset(); + }); + + it('should not register the provider if OTEL_LOGS_EXPORTER contains none', async () => { + process.env.OTEL_LOGS_EXPORTER = 'console,none'; + const sdk = startNodeSDK({}); + assert.strictEqual( + stubLogger.args[0][0], + 'OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.' + ); + + assert.ok( + setGlobalLoggerProviderSpy.callCount === 0, + 'logger provider should not have changed' + ); + await sdk.shutdown(); + }); + + it('should set up all allowed exporters', async () => { + process.env.OTEL_LOGS_EXPORTER = 'console,otlp'; + const sdk = startNodeSDK({}); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert.ok(sharedState.registeredLogRecordProcessors.length === 2); + assert.ok( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + ConsoleLogRecordExporter + ); + assert.ok( + sharedState.registeredLogRecordProcessors[0] instanceof + SimpleLogRecordProcessor + ); + // defaults to http/protobuf + assert.ok( + sharedState.registeredLogRecordProcessors[1]._exporter instanceof + OTLPProtoLogExporter + ); + assert.ok( + sharedState.registeredLogRecordProcessors[1] instanceof + BatchLogRecordProcessor + ); + await sdk.shutdown(); + }); + + it('should use OTEL_EXPORTER_OTLP_LOGS_PROTOCOL for otlp protocol', async () => { + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'grpc'; + const sdk = startNodeSDK({}); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert.ok(sharedState.registeredLogRecordProcessors.length === 1); + assert.ok( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPGrpcLogExporter + ); + await sdk.shutdown(); + }); + + it('should use OTLPHttpLogExporter when http/json is set', async () => { + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'http/json'; + const sdk = startNodeSDK({}); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert.ok(sharedState.registeredLogRecordProcessors.length === 1); + assert.ok( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPHttpLogExporter + ); + await sdk.shutdown(); + }); + + it('should fall back to OTEL_EXPORTER_OTLP_PROTOCOL', async () => { + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + process.env.OTEL_EXPORTER_OTLP_PROTOCOL = 'grpc'; + const sdk = startNodeSDK({}); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert.ok(sharedState.registeredLogRecordProcessors.length === 1); + assert.ok( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPGrpcLogExporter + ); + await sdk.shutdown(); + }); + + it('should fall back to http/protobuf if invalid protocol is set', async () => { + process.env.OTEL_LOGS_EXPORTER = 'otlp'; + process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'grpc2'; + const sdk = startNodeSDK({}); + + const loggerProvider = logs.getLoggerProvider(); + const sharedState = (loggerProvider as any)['_sharedState']; + assert.ok(sharedState.registeredLogRecordProcessors.length === 1); + assert.ok( + sharedState.registeredLogRecordProcessors[0]._exporter instanceof + OTLPProtoLogExporter + ); + await sdk.shutdown(); + }); + }); }); function assertDefaultContextManagerRegistered() { @@ -65,3 +604,11 @@ function assertDefaultContextManagerRegistered() { AsyncLocalStorageContextManager.name ); } + +function assertDefaultPropagatorRegistered() { + assert.deepStrictEqual(propagation.fields(), [ + 'traceparent', + 'tracestate', + 'baggage', + ]); +} diff --git a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts index 0938b6090fb..381f04af5cd 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts @@ -21,7 +21,7 @@ import { ATTR_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; -import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAMESPACE } from '../semconv'; +import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAMESPACE } from '../../src/semconv'; /** * Test utility method to validate a service resource From 0be256e89267c551b96a7a262b7ed95c1310df0f Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 12 Feb 2026 10:30:58 -0500 Subject: [PATCH 2/8] remove default propagator --- .../packages/opentelemetry-sdk-node/src/start.ts | 14 +------------- .../packages/opentelemetry-sdk-node/src/utils.ts | 1 - 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/experimental/packages/opentelemetry-sdk-node/src/start.ts b/experimental/packages/opentelemetry-sdk-node/src/start.ts index 88071db20a0..0b3e4e60d02 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/start.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/start.ts @@ -45,11 +45,6 @@ import { } from '@opentelemetry/resources'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { ATTR_SERVICE_INSTANCE_ID } from './semconv'; -import { - CompositePropagator, - W3CBaggagePropagator, - W3CTraceContextPropagator, -} from '@opentelemetry/core'; /** * @experimental Function to start the OpenTelemetry Node SDK @@ -111,18 +106,11 @@ function create( const propagator = sdkOptions?.textMapPropagator === null - ? null // null means don't set. + ? null : (sdkOptions?.textMapPropagator ?? getPropagatorFromConfiguration(config)); if (propagator) { components.propagator = propagator; - } else if (propagator === undefined) { - components.propagator = new CompositePropagator({ - propagators: [ - new W3CTraceContextPropagator(), - new W3CBaggagePropagator(), - ], - }); } const logProcessors = getLogRecordProcessorsFromConfiguration(config); diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index c93f8d1b1a4..631085f50f5 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -103,7 +103,6 @@ export function getResourceFromConfiguration( } export function getResourceDetectorsFromEnv(): Array { - console.log("AAAAAAAAA"); // When updating this list, make sure to also update the section `resourceDetectors` on README. const resourceDetectors = new Map([ [RESOURCE_DETECTOR_HOST, hostDetector], From c9f95ccd76c5e78807517a33c89dd22ce57a8c27 Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 12 Feb 2026 10:35:03 -0500 Subject: [PATCH 3/8] changelog --- experimental/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 81cd5677f71..44903270b30 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -18,7 +18,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm * feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telemetry/opentelemetry-js/pull/6304) @maryliag * feat(instrumentation): use the `internals: true` option with import-in-the-middle hook, allowing instrumentations to hook internal files in ES modules [#6344](https://github.com/open-telemetry/opentelemetry-js/pull/6344) @trentm -* feat(opentelemetry-sdk-node): set log provider for experimental start [#x](https://github.com/open-telemetry/opentelemetry-js/pull/x) @maryliag +* feat(opentelemetry-sdk-node): set log provider for experimental start [#6407](https://github.com/open-telemetry/opentelemetry-js/pull/6407) @maryliag ### :bug: Bug Fixes From ad9a4dc1371c2724ccda68a17688fdaddd2c2ef8 Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 12 Feb 2026 10:42:38 -0500 Subject: [PATCH 4/8] lint and test fix --- .../packages/opentelemetry-sdk-node/test/start.test.ts | 10 +--------- .../test/util/resource-assertions.ts | 5 ++++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts index 2822287c2e9..58506c4614f 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts @@ -96,7 +96,7 @@ describe('startNodeSDK', function () { // These are minimal OTel functionality and always registered. assertDefaultContextManagerRegistered(); - assertDefaultPropagatorRegistered(); + assert.deepStrictEqual(propagation.fields(), []); assert.ok( setGlobalLoggerProviderSpy.called === false, @@ -604,11 +604,3 @@ function assertDefaultContextManagerRegistered() { AsyncLocalStorageContextManager.name ); } - -function assertDefaultPropagatorRegistered() { - assert.deepStrictEqual(propagation.fields(), [ - 'traceparent', - 'tracestate', - 'baggage', - ]); -} diff --git a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts index 381f04af5cd..ca7159206b1 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/util/resource-assertions.ts @@ -21,7 +21,10 @@ import { ATTR_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; -import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAMESPACE } from '../../src/semconv'; +import { + ATTR_SERVICE_INSTANCE_ID, + ATTR_SERVICE_NAMESPACE, +} from '../../src/semconv'; /** * Test utility method to validate a service resource From 890e138fbf014604810d8e3b656893c1b3161a65 Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 12 Feb 2026 12:43:12 -0500 Subject: [PATCH 5/8] add more tests --- .../opentelemetry-sdk-node/test/start.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts index 58506c4614f..07ccbfd4be1 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts @@ -53,7 +53,8 @@ import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; -import { ATTR_HOST_NAME, ATTR_PROCESS_PID } from '../src/semconv'; +import { ATTR_HOST_NAME, ATTR_PROCESS_PID, ATTR_SERVICE_INSTANCE_ID } from '../src/semconv'; +import { ATTR_OS_TYPE } from '@opentelemetry/resources/src/semconv'; describe('startNodeSDK', function () { let setGlobalLoggerProviderSpy: Sinon.SinonSpy; @@ -325,6 +326,19 @@ describe('startNodeSDK', function () { assert.equal(resource.attributes[ATTR_HOST_NAME], undefined); }); + it('have node resource detectors with OTEL_NODE_RESOURCE_DETECTORS as all', async () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'all'; + const configFactory: ConfigFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + const resource = setupResource(config, {}); + await resource.waitForAsyncAttributes?.(); + + assert.notEqual(resource.attributes[ATTR_PROCESS_PID], undefined); + assert.notEqual(resource.attributes[ATTR_HOST_NAME], undefined); + assert.notEqual(resource.attributes[ATTR_OS_TYPE], undefined); + assert.notEqual(resource.attributes[ATTR_SERVICE_INSTANCE_ID], undefined); + }); + it('should configure resources from config file', async () => { process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = 'test/fixtures/resources.yaml'; From c3f79e46277590202fe0cb0e61f003b91485d598 Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 12 Feb 2026 12:54:39 -0500 Subject: [PATCH 6/8] fix lint --- .../packages/opentelemetry-sdk-node/test/start.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts index 07ccbfd4be1..2a5980f99d7 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts @@ -53,7 +53,11 @@ import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc'; -import { ATTR_HOST_NAME, ATTR_PROCESS_PID, ATTR_SERVICE_INSTANCE_ID } from '../src/semconv'; +import { + ATTR_HOST_NAME, + ATTR_PROCESS_PID, + ATTR_SERVICE_INSTANCE_ID, +} from '../src/semconv'; import { ATTR_OS_TYPE } from '@opentelemetry/resources/src/semconv'; describe('startNodeSDK', function () { From 7a8fdaac03e362ec4140e9079515cc31a79a0381 Mon Sep 17 00:00:00 2001 From: maryliag Date: Wed, 18 Feb 2026 10:34:13 -0500 Subject: [PATCH 7/8] updates from feedback --- .../opentelemetry-sdk-node/src/utils.ts | 50 ++++++++++++------- .../opentelemetry-sdk-node/test/start.test.ts | 47 +++++++++++------ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index 631085f50f5..df9b9cbc4a8 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -551,7 +551,7 @@ export function getBatchLogRecordProcessorFromEnv( export function getLogRecordExporter( exporter: LogRecordExporterModel -): LogRecordExporter { +): LogRecordExporter | undefined { if (exporter.otlp_http) { const encoding = exporter.otlp_http.encoding; if (encoding === 'json') { @@ -563,19 +563,34 @@ export function getLogRecordExporter( }); } if (encoding === 'protobuf') { - return new OTLPProtoLogExporter(); + return new OTLPProtoLogExporter({ + compression: + exporter.otlp_http.compression === 'gzip' + ? CompressionAlgorithm.GZIP + : CompressionAlgorithm.NONE, + }); } diag.warn( `Unsupported OTLP logs encoding: ${encoding}. Using http/protobuf.` ); - return new OTLPProtoLogExporter(); + return new OTLPProtoLogExporter({ + compression: + exporter.otlp_http.compression === 'gzip' + ? CompressionAlgorithm.GZIP + : CompressionAlgorithm.NONE, + }); } else if (exporter.otlp_grpc) { - return new OTLPGrpcLogExporter(); + return new OTLPGrpcLogExporter({ + compression: + exporter.otlp_grpc.compression === 'gzip' + ? CompressionAlgorithm.GZIP + : CompressionAlgorithm.NONE, + }); } else if (exporter.console) { return new ConsoleLogRecordExporter(); } - diag.warn(`Unsupported Exporter value. Using OTLP http/protobuf.`); - return new OTLPProtoLogExporter(); + diag.warn(`Unsupported Exporter value. No Log Record Exporter registered`); + return undefined; } export function getLogRecordProcessorsFromConfiguration( @@ -584,24 +599,23 @@ export function getLogRecordProcessorsFromConfiguration( const logRecordProcessors: LogRecordProcessor[] = []; config.logger_provider?.processors?.forEach(processor => { if (processor.batch) { - logRecordProcessors.push( - new BatchLogRecordProcessor( - getLogRecordExporter(processor.batch.exporter), - { + const exporter = getLogRecordExporter(processor.batch.exporter); + if (exporter) { + logRecordProcessors.push( + new BatchLogRecordProcessor(exporter, { maxQueueSize: processor.batch.max_queue_size, maxExportBatchSize: processor.batch.max_export_batch_size, scheduledDelayMillis: processor.batch.schedule_delay, exportTimeoutMillis: processor.batch.export_timeout, - } - ) - ); + }) + ); + } } if (processor.simple) { - logRecordProcessors.push( - new SimpleLogRecordProcessor( - getLogRecordExporter(processor.simple.exporter) - ) - ); + const exporter = getLogRecordExporter(processor.simple.exporter); + if (exporter) { + logRecordProcessors.push(new SimpleLogRecordProcessor(exporter)); + } } }); if (logRecordProcessors.length > 0) { diff --git a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts index 2a5980f99d7..62db9472e9f 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts @@ -48,6 +48,7 @@ import { import { ConfigFactory, createConfigFactory, + LogRecordExporterModel, } from '@opentelemetry/configuration'; import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; @@ -59,6 +60,7 @@ import { ATTR_SERVICE_INSTANCE_ID, } from '../src/semconv'; import { ATTR_OS_TYPE } from '@opentelemetry/resources/src/semconv'; +import { getLogRecordExporter, setupContextManager } from '../src/utils'; describe('startNodeSDK', function () { let setGlobalLoggerProviderSpy: Sinon.SinonSpy; @@ -91,7 +93,7 @@ describe('startNodeSDK', function () { Sinon.restore(); }); - describe('Basic Registration', () => { + describe('Basic Registration', function () { it('should not register more than the minimal SDK components', async () => { // need to set these to none, since the default value is 'otlp' process.env.OTEL_TRACES_EXPORTER = 'none'; @@ -111,7 +113,7 @@ describe('startNodeSDK', function () { await sdk.shutdown(); }); - it('should register a diag logger with OTEL_LOG_LEVEL', () => { + it('should register a diag logger with OTEL_LOG_LEVEL', async () => { process.env.OTEL_LOG_LEVEL = 'ERROR'; const spy = Sinon.spy(diag, 'setLogger'); @@ -123,10 +125,10 @@ describe('startNodeSDK', function () { logLevel: DiagLogLevel.ERROR, }); - sdk.shutdown(); + await sdk.shutdown(); }); - it('should register a diag logger with INFO with OTEL_LOG_LEVEL unset', () => { + it('should register a diag logger with INFO with OTEL_LOG_LEVEL unset', async () => { delete process.env.OTEL_LOG_LEVEL; const spy = Sinon.spy(diag, 'setLogger'); @@ -137,7 +139,7 @@ describe('startNodeSDK', function () { assert.deepStrictEqual(spy.args[0][1], { logLevel: DiagLogLevel.INFO, }); - sdk.shutdown(); + await sdk.shutdown(); }); it('should register a propagator if only a propagator is provided', async () => { @@ -186,27 +188,27 @@ describe('startNodeSDK', function () { }); }); - it('should return NOOP_SDK when disabled is true', () => { + it('should return NOOP_SDK when disabled is true', async () => { const info = Sinon.spy(diag, 'info'); process.env.OTEL_SDK_DISABLED = 'true'; const sdk = startNodeSDK({}); Sinon.assert.calledWith(info, 'OpenTelemetry SDK is disabled'); - sdk.shutdown(); + await sdk.shutdown(); }); - it('should return NOOP_SDK when disabled is true', () => { + it('should return NOOP_SDK when disabled is true', async () => { process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = 'test/fixtures/kitchen-sink.yaml'; const sdk = startNodeSDK({}); assertDefaultContextManagerRegistered(); - sdk.shutdown(); + await sdk.shutdown(); }); - it('should register a diag logger as info as default', () => { + it('should register a diag logger as info as default', async () => { const spy = Sinon.spy(diag, 'setLogger'); const sdk = startNodeSDK({}); @@ -216,7 +218,7 @@ describe('startNodeSDK', function () { logLevel: DiagLogLevel.INFO, }); - sdk.shutdown(); + await sdk.shutdown(); }); it('should register a logger provider if multiple log record processors are provided', async () => { @@ -253,7 +255,7 @@ describe('startNodeSDK', function () { await sdk.shutdown(); }); - describe('setupResources', async () => { + describe('setupResources', async function () { beforeEach(() => { process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.instance.id=627cc493,service.name=my-service,service.namespace=default,service.version=0.0.1'; @@ -428,7 +430,7 @@ describe('startNodeSDK', function () { }); }); - describe('configureServiceName', async () => { + describe('configureServiceName', async function () { it('should configure service name via OTEL_SERVICE_NAME env var', async () => { process.env.OTEL_SERVICE_NAME = 'env-set-name'; process.env.OTEL_RESOURCE_ATTRIBUTES = @@ -459,7 +461,7 @@ describe('startNodeSDK', function () { }); }); - describe('configureServiceInstanceId', async () => { + describe('configureServiceInstanceId', async function () { it('should configure service instance id via OTEL_RESOURCE_ATTRIBUTES env var', async () => { process.env.OTEL_RESOURCE_ATTRIBUTES = 'service.instance.id=627cc493,service.name=my-service,service.namespace'; @@ -501,7 +503,7 @@ describe('startNodeSDK', function () { }); }); - describe('configuring logger provider from env', () => { + describe('configuring logger provider from env', function () { let stubLogger: Sinon.SinonStub; beforeEach(() => { @@ -614,6 +616,21 @@ describe('startNodeSDK', function () { await sdk.shutdown(); }); }); + + describe('tests to increase code coverage', function () { + it('should return undefined for invalid log record exporter model', async () => { + const exporter: LogRecordExporterModel = {}; + assert.equal(getLogRecordExporter(exporter), undefined); + }); + + it('', async () => { + setupContextManager(null); + assert.equal( + context['_getContextManager']().constructor.name, + 'NoopContextManager' + ); + }); + }); }); function assertDefaultContextManagerRegistered() { From 6b843ec3190e71e1038b101ffaed3a0d44fdb72f Mon Sep 17 00:00:00 2001 From: maryliag Date: Thu, 19 Feb 2026 11:18:50 -0500 Subject: [PATCH 8/8] changelog --- experimental/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 71b88c50e9d..54848d053bd 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -34,7 +34,6 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features * feat(configuration): add Prometheus exporter support [#6400](https://github.com/open-telemetry/opentelemetry-js/pull/6400) @MikeGoldsmith - * feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm * feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telemetry/opentelemetry-js/pull/6304) @maryliag * feat(instrumentation): use the `internals: true` option with import-in-the-middle hook, allowing instrumentations to hook internal files in ES modules [#6344](https://github.com/open-telemetry/opentelemetry-js/pull/6344) @trentm