Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d5dff48
feat(configuration): add resource detection parsing
MikeGoldsmith Feb 20, 2026
a069e5e
chore: update changelog with PR number for resource detection parsing
MikeGoldsmith Feb 20, 2026
82639ab
fix(configuration): fix lint errors
MikeGoldsmith Feb 20, 2026
89623f6
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MikeGoldsmith Feb 20, 2026
fd29d73
fix(configuration): remove unreachable null guard in parseConfigFile
MikeGoldsmith Feb 20, 2026
4111ccb
refactor(configuration): use for loop in parseDetectionDevelopment
MikeGoldsmith Feb 23, 2026
97b2d7f
Merge branch 'main' of github.com:open-telemetry/opentelemetry-js int…
MikeGoldsmith Feb 24, 2026
3c9c238
refactor(configuration): remove node_resource_detectors in favour of …
MikeGoldsmith Mar 2, 2026
8300978
Merge upstream/main into mike/resource-detection-parsing
MikeGoldsmith Mar 3, 2026
3e6a8f7
feat(configuration): add os and env resource detector support
MikeGoldsmith Mar 3, 2026
965013c
fix(configuration): update start.test.ts for os detector in all
MikeGoldsmith Mar 3, 2026
76f01dc
fix(configuration): move env detector to last position
MikeGoldsmith Mar 3, 2026
ccadf11
merge upstream/main into mike/resource-detection-parsing
MikeGoldsmith Mar 9, 2026
dd5d7bc
fix lint: use import type for ExperimentalResourceDetector
MikeGoldsmith Mar 9, 2026
9b3bdb0
fix(configuration): address review feedback from trentm
MikeGoldsmith Mar 10, 2026
bb6d2fd
Merge branch 'main' into mike/resource-detection-parsing
trentm Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
59 changes: 58 additions & 1 deletion experimental/packages/configuration/src/FileConfigFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 15 additions & 4 deletions experimental/packages/configuration/src/models/resourceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
78 changes: 77 additions & 1 deletion experimental/packages/configuration/test/ConfigFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,9 @@ resource:
- process.command_args
detectors:
- container:
- env:
- host:
- os:
- process:
- service:
schema_url: https://opentelemetry.io/schemas/1.16.0
2 changes: 2 additions & 0 deletions experimental/packages/opentelemetry-sdk-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
MikeGoldsmith marked this conversation as resolved.

For example, to enable only the `env`, `host` detectors:

```shell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
35 changes: 10 additions & 25 deletions experimental/packages/opentelemetry-sdk-node/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,31 +128,16 @@ export function getResourceDetectorsFromEnv(): Array<ResourceDetector> {
export function getResourceDetectorsFromConfiguration(
config: ConfigurationModel
): Array<ResourceDetector> {
// When updating this list, make sure to also update the section `resourceDetectors` on README.
const resourceDetectors = new Map<string, ResourceDetector>([
[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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We are allowed to have a breaking change in v3. I wonder if we want to change our (currently experimental, I think?) serviceInstanceIdDetector and deprecate it in favour of a serviceDetector that does the same thing.

(I'm not sure if the intent is that OTEL_SERVICE_NAME is handled by service or by env.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree this is worth discussing, but feels like a separate concern from this PR. Happy to open a follow-up issue to track renaming serviceInstanceIdDetectorserviceDetector for v3 if that's the direction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Absolutely a separate PR kind of thing.
I'm curious for @maryliag's opinion. I.e. whether changes in OTel JS here are worth it to have a service resource detector be closer in naming and scope to other languages (or at least closer to what I assume OTel Java does).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like the idea of a serviceDetector that handles everything service related, meaning instance ID and name, and making it clear the priority of where the value comes from, so it's worth opening a new issue/PR to handle that.

To answer your other question:

OTEL_SERVICE_NAME is handled by service or by env

I think being handled by the service would make more obvious to users, because is in the name, and we can make the priority clear of where the value comes from.
Using a serviceDetector would also help with the decision between the random uuid and defined service instance ID, where today depending on the order the users adds (serviceInstanceId, env VS env, serviceInstanceId) it returns different results.
That would be a breaking change, so as long as we make that clear to users, then should be fine

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks both - I like the idea of replacing serviceInstanceDetecto -> serviceDetector and simplifying to cover name and ID.

I opened the follow-up issue to track doing that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FWIW, I spent some time looking at OTel Java and Python implementations, and ... gave up after a little bit. Python doesn't have anything for service.instance.id I don't think, yet.

OTel Java has a ServiceInstanceIdResourceProvider (https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/resources/ServiceInstanceIdResourceProvider.java#L20-L47). It and the EnvironmentResourceProvider are using .order() SPI things (with values -1 and MAX_INT) to handle priority.

It also has a ServiceResourceDetector (with name "service") in the declarative config code (https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ServiceResourceDetector.java#L18) that is slightly different in that it handles service.name and service.instance.id.

Anyway, clarity to seek on another day.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the notes @trentm - I've been helping out on the Java implementation and am leading the Python implementation too (which is still very early).

I'd like to consolidate on a consistent pattern too and will keep an eye on it the linked issue 👍🏻

if (detector.env != null) result.push(envDetector);
return result;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading
Loading