diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index defff18a1a9..1411f965eba 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -12,6 +12,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features +* feat(configuration): add resource detection parsing [#6435](https://github.com/open-telemetry/opentelemetry-js/pull/6435) @MikeGoldsmith * feat(configuration): export interfaces required in other packages [#6462](https://github.com/open-telemetry/opentelemetry-js/pull/6462) @maryliag ### :bug: Bug Fixes diff --git a/experimental/packages/configuration/src/EnvironmentConfigFactory.ts b/experimental/packages/configuration/src/EnvironmentConfigFactory.ts index e8186e5d801..692d53e9146 100644 --- a/experimental/packages/configuration/src/EnvironmentConfigFactory.ts +++ b/experimental/packages/configuration/src/EnvironmentConfigFactory.ts @@ -30,6 +30,7 @@ import { initializeDefaultTracerProviderConfiguration } from './models/tracerPro import type { BatchLogRecordProcessor } from './models/loggerProviderModel'; import { initializeDefaultLoggerProviderConfiguration } from './models/loggerProviderModel'; import { getGrpcTlsConfig, getHttpTlsConfig } from './utils'; +import type { ExperimentalResourceDetector } from './models/resourceModel'; /** * EnvironmentConfigProvider provides a configuration based on environment variables. @@ -46,13 +47,6 @@ export class EnvironmentConfigFactory implements ConfigFactory { this._config.log_level = logLevel; } - const nodeResourceDetectors = getStringListFromEnv( - 'OTEL_NODE_RESOURCE_DETECTORS' - ); - if (nodeResourceDetectors) { - this._config.node_resource_detectors = nodeResourceDetectors; - } - setResources(this._config); setAttributeLimits(this._config); setPropagators(this._config); @@ -104,6 +98,31 @@ export function setResources(config: ConfigurationModel): void { } } } + + const nodeDetectors = getStringListFromEnv('OTEL_NODE_RESOURCE_DETECTORS'); + if ( + nodeDetectors && + nodeDetectors.length > 0 && + !nodeDetectors.includes('none') + ) { + const all = nodeDetectors.includes('all'); + const detectors: ExperimentalResourceDetector[] = []; + if (all || nodeDetectors.includes('container')) + detectors.push({ container: {} }); + if (all || nodeDetectors.includes('host')) detectors.push({ host: {} }); + if (all || nodeDetectors.includes('os')) detectors.push({ os: {} }); + if (all || nodeDetectors.includes('process')) + detectors.push({ process: {} }); + if (all || nodeDetectors.includes('serviceinstance')) + detectors.push({ service: {} }); + if (all || nodeDetectors.includes('env')) detectors.push({ env: {} }); + if (detectors.length > 0) { + if (config.resource['detection/development'] == null) { + config.resource['detection/development'] = {}; + } + config.resource['detection/development'].detectors = detectors; + } + } } export function setAttributeLimits(config: ConfigurationModel): void { diff --git a/experimental/packages/configuration/src/FileConfigFactory.ts b/experimental/packages/configuration/src/FileConfigFactory.ts index 69727d06001..b49d4fe33bf 100644 --- a/experimental/packages/configuration/src/FileConfigFactory.ts +++ b/experimental/packages/configuration/src/FileConfigFactory.ts @@ -38,7 +38,11 @@ import type { LogRecordProcessor, } from './models/loggerProviderModel'; import { initializeDefaultLoggerProviderConfiguration } from './models/loggerProviderModel'; -import type { AttributeNameValue } from './models/resourceModel'; +import type { + AttributeNameValue, + ExperimentalResourceDetection, + ExperimentalResourceDetector, +} from './models/resourceModel'; import type { Aggregation, CardinalityLimits, @@ -130,6 +134,13 @@ export function parseConfigFile(config: ConfigurationModel) { parsedContent['resource']?.['attributes'], parsedContent['resource']?.['attributes_list'] ); + + const detectionConfig = + parsedContent['resource']?.['detection/development']; + if (detectionConfig) { + config.resource!['detection/development'] = + parseDetectionDevelopment(detectionConfig); + } setAttributeLimits(config, parsedContent['attribute_limits']); setPropagator(config, parsedContent['propagator']); setTracerProvider(config, parsedContent['tracer_provider']); @@ -213,6 +224,52 @@ export function setResourceAttributes( } } +function parseDetectionDevelopment( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + detection: any +): ExperimentalResourceDetection { + const result: ExperimentalResourceDetection = {}; + + if (detection['attributes']) { + result.attributes = {}; + const included = detection['attributes']['included']; + if (Array.isArray(included)) { + result.attributes.included = included.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (v: any) => typeof v === 'string' + ); + } + const excluded = detection['attributes']['excluded']; + if (Array.isArray(excluded)) { + result.attributes.excluded = excluded.filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (v: any) => typeof v === 'string' + ); + } + } + + if (Array.isArray(detection['detectors'])) { + result.detectors = []; + for (let i = 0; i < detection['detectors'].length; i++) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const d: any = detection['detectors'][i]; + if (typeof d !== 'object' || d === null) { + continue; + } + const detector: ExperimentalResourceDetector = {}; + if ('container' in d) detector.container = d.container ?? {}; + if ('env' in d) detector.env = d.env ?? {}; + if ('host' in d) detector.host = d.host ?? {}; + if ('os' in d) detector.os = d.os ?? {}; + if ('process' in d) detector.process = d.process ?? {}; + if ('service' in d) detector.service = d.service ?? {}; + result.detectors.push(detector); + } + } + + return result; +} + export function setAttributeLimits( config: ConfigurationModel, attrLimits: AttributeLimits diff --git a/experimental/packages/configuration/src/models/configModel.ts b/experimental/packages/configuration/src/models/configModel.ts index a6bd1f5bd18..e436927d8cd 100644 --- a/experimental/packages/configuration/src/models/configModel.ts +++ b/experimental/packages/configuration/src/models/configModel.ts @@ -23,12 +23,6 @@ export interface ConfigurationModel { */ log_level?: number; - /** - * Node resource detectors - * If omitted, all is used. - */ - node_resource_detectors?: string[]; - /** * Configure resource for all signals. * If omitted, the default resource is used. diff --git a/experimental/packages/configuration/src/models/resourceModel.ts b/experimental/packages/configuration/src/models/resourceModel.ts index dc6e06efaf0..2928aaf3cd1 100644 --- a/experimental/packages/configuration/src/models/resourceModel.ts +++ b/experimental/packages/configuration/src/models/resourceModel.ts @@ -67,7 +67,7 @@ export interface ExperimentalResourceDetection { * Resource detector names are dependent on the SDK language ecosystem. Please consult documentation for each respective language. * If omitted or null, no resource detectors are enabled. */ - detectors?: ExperimentalResourceDetector; + detectors?: ExperimentalResourceDetector[]; } export interface ExperimentalResourceDetector { @@ -77,18 +77,29 @@ export interface ExperimentalResourceDetector { container?: object; /** - * Enable the host resource detector, which populates host.* and os.* attributes. + * Enable the environment variable resource detector (Node.js only, no spec equivalent). + * Reads OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables. + */ + env?: object; + + /** + * Enable the host resource detector, which populates host.* attributes. */ host?: object; + /** + * Enable the OS resource detector (Node.js only, no spec equivalent). + * Populates os.type and os.version attributes. + */ + os?: object; + /** * Enable the process resource detector, which populates process.* attributes. */ process?: object; /** - * Enable the service detector, which populates service.name based on the OTEL_SERVICE_NAME - * environment variable and service.instance.id. + * Enable the service detector, which populates service.instance.id. */ service?: object; } diff --git a/experimental/packages/configuration/test/ConfigFactory.test.ts b/experimental/packages/configuration/test/ConfigFactory.test.ts index d4e3f6ed401..336bf3e44aa 100644 --- a/experimental/packages/configuration/test/ConfigFactory.test.ts +++ b/experimental/packages/configuration/test/ConfigFactory.test.ts @@ -244,6 +244,20 @@ const configFromKitchenSinkFile: ConfigurationModel = { value: '1.0.0', }, ], + 'detection/development': { + attributes: { + included: ['process.*'], + excluded: ['process.command_args'], + }, + detectors: [ + { container: {} }, + { env: {} }, + { host: {} }, + { os: {} }, + { process: {} }, + { service: {} }, + ], + }, }, attribute_limits: { attribute_count_limit: 128, @@ -989,7 +1003,69 @@ describe('ConfigFactory', function () { process.env.OTEL_NODE_RESOURCE_DETECTORS = 'env,host, serviceinstance'; const expectedConfig: ConfigurationModel = { ...defaultConfig, - node_resource_detectors: ['env', 'host', 'serviceinstance'], + resource: { + 'detection/development': { + detectors: [{ host: {} }, { service: {} }, { env: {} }], + }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should map OTEL_NODE_RESOURCE_DETECTORS=all to all detectors', function () { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'all'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + resource: { + 'detection/development': { + detectors: [ + { container: {} }, + { host: {} }, + { os: {} }, + { process: {} }, + { service: {} }, + { env: {} }, + ], + }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should not set detection/development for OTEL_NODE_RESOURCE_DETECTORS=none', function () { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'none'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should map OTEL_NODE_RESOURCE_DETECTORS=os to os detector', function () { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'os'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + resource: { + 'detection/development': { + detectors: [{ os: {} }], + }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should map OTEL_NODE_RESOURCE_DETECTORS=env to env detector', function () { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'env'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + resource: { + 'detection/development': { + detectors: [{ env: {} }], + }, + }, }; const configFactory = createConfigFactory(); assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); diff --git a/experimental/packages/configuration/test/fixtures/kitchen-sink.yaml b/experimental/packages/configuration/test/fixtures/kitchen-sink.yaml index 837d614cd32..8a2d0309ff6 100644 --- a/experimental/packages/configuration/test/fixtures/kitchen-sink.yaml +++ b/experimental/packages/configuration/test/fixtures/kitchen-sink.yaml @@ -428,7 +428,9 @@ resource: - process.command_args detectors: - container: + - env: - host: + - os: - process: - service: schema_url: https://opentelemetry.io/schemas/1.16.0 \ No newline at end of file diff --git a/experimental/packages/opentelemetry-sdk-node/README.md b/experimental/packages/opentelemetry-sdk-node/README.md index c38a8e936ae..db138bfeeb7 100644 --- a/experimental/packages/opentelemetry-sdk-node/README.md +++ b/experimental/packages/opentelemetry-sdk-node/README.md @@ -146,6 +146,8 @@ If `resourceDetectors` was not set, you can also use the environment variable `O - **NOTE:** future versions of `@opentelemetry/sdk-node` may include additional detectors that will be covered by this scope. - `none` - disable resource detection +**NOTE:** `env` and `os` are Node.js-specific detectors with no equivalent in the [OpenTelemetry declarative configuration spec](https://github.com/open-telemetry/opentelemetry-configuration). They are supported when using the `detection/development` block in a declarative config file. + For example, to enable only the `env`, `host` detectors: ```shell diff --git a/experimental/packages/opentelemetry-sdk-node/src/start.ts b/experimental/packages/opentelemetry-sdk-node/src/start.ts index e49db71f07a..49c4e94b4df 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/start.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/start.ts @@ -126,7 +126,7 @@ export function setupResource( if (sdkOptions.resourceDetectors != null) { resourceDetectors = sdkOptions.resourceDetectors; - } else if (config.node_resource_detectors) { + } else if (config.resource?.['detection/development']?.detectors) { resourceDetectors = getResourceDetectorsFromConfiguration(config); } diff --git a/experimental/packages/opentelemetry-sdk-node/src/utils.ts b/experimental/packages/opentelemetry-sdk-node/src/utils.ts index 12852aa25e6..917a3c76d3c 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/utils.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/utils.ts @@ -128,31 +128,16 @@ 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 || []; + const detectors = config.resource?.['detection/development']?.detectors ?? []; + + return detectors.flatMap(detector => { + const result: ResourceDetector[] = []; + if (detector.host != null) result.push(hostDetector); + if (detector.os != null) result.push(osDetector); + if (detector.process != null) result.push(processDetector); + if (detector.service != null) result.push(serviceInstanceIdDetector); + if (detector.env != null) result.push(envDetector); + return result; }); } diff --git a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts index 0825c7fd66e..cd8dc02b88b 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/start.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/start.test.ts @@ -329,8 +329,8 @@ describe('startNodeSDK', function () { 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); + assert.notEqual(resource.attributes[ATTR_OS_TYPE], undefined); }); it('should configure resources from config file', async () => { diff --git a/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts b/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts index cd68f0bc50d..23599bace97 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/utils.test.ts @@ -9,11 +9,19 @@ import { getPropagatorFromConfiguration, getLoggerProviderConfigFromEnv, getBatchLogRecordProcessorConfigFromEnv, + getResourceDetectorsFromConfiguration, } from '../src/utils'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { diag } from '@opentelemetry/api'; import type { ConfigurationModel } from '@opentelemetry/configuration'; +import { + envDetector, + hostDetector, + osDetector, + processDetector, + serviceInstanceIdDetector, +} from '@opentelemetry/resources'; import type { LoggerProviderConfig } from '@opentelemetry/sdk-logs'; describe('getPropagatorFromEnv', function () { @@ -401,3 +409,86 @@ describe('getBatchLogRecordProcessorConfigFromEnv', function () { sinon.assert.callCount(warnStub, 4); }); }); + +describe('getResourceDetectorsFromConfiguration', function () { + it('returns empty array when detection/development is not set', function () { + const config: ConfigurationModel = {}; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), []); + }); + + it('returns empty array when detectors array is empty', function () { + const config: ConfigurationModel = { + resource: { 'detection/development': { detectors: [] } }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), []); + }); + + it('maps env detector object to envDetector', function () { + const config: ConfigurationModel = { + resource: { 'detection/development': { detectors: [{ env: {} }] } }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), [ + envDetector, + ]); + }); + + it('maps host detector object to hostDetector', function () { + const config: ConfigurationModel = { + resource: { 'detection/development': { detectors: [{ host: {} }] } }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), [ + hostDetector, + ]); + }); + + it('maps os detector object to osDetector', function () { + const config: ConfigurationModel = { + resource: { 'detection/development': { detectors: [{ os: {} }] } }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), [ + osDetector, + ]); + }); + + it('maps process detector object to processDetector', function () { + const config: ConfigurationModel = { + resource: { 'detection/development': { detectors: [{ process: {} }] } }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), [ + processDetector, + ]); + }); + + it('maps service detector object to serviceInstanceIdDetector', function () { + const config: ConfigurationModel = { + resource: { 'detection/development': { detectors: [{ service: {} }] } }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), [ + serviceInstanceIdDetector, + ]); + }); + + it('silently skips container detector (no JS implementation)', function () { + const config: ConfigurationModel = { + resource: { + 'detection/development': { detectors: [{ container: {} }] }, + }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), []); + }); + + it('maps multiple detector objects in order', function () { + const config: ConfigurationModel = { + resource: { + 'detection/development': { + detectors: [{ host: {} }, { process: {} }, { service: {} }], + }, + }, + }; + assert.deepStrictEqual(getResourceDetectorsFromConfiguration(config), [ + hostDetector, + processDetector, + serviceInstanceIdDetector, + ]); + }); +});