Skip to content

Commit f1dac69

Browse files
committed
fix(opentelemetry): Respect OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES (#20509)
This uses the string passed into `getSentryResource` as a fallback, preferring instead to use the value in `env.OTEL_SERVICE_NAME` if set, or the `service.name` field in the comma-delimited key=value pairs in `env.OTEL_RESOURCE_ATTRIBUTES` pairs. Additional `env.OTEL_RESOURCE_ATTRIBUTES` are also attached to the resource attributes. fix: js-2280 fix: #20502
1 parent 9956476 commit f1dac69

2 files changed

Lines changed: 173 additions & 2 deletions

File tree

packages/opentelemetry/src/resource.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,58 @@ class SentryResource {
3939
}
4040
}
4141

42+
/**
43+
* Parses `OTEL_RESOURCE_ATTRIBUTES` env var (comma-separated `key=value` pairs).
44+
* Values are URL-decoded per the OTel spec.
45+
*/
46+
function parseOtelResourceAttributes(raw: string | undefined): Attributes {
47+
if (!raw) {
48+
return {};
49+
}
50+
const result: Attributes = {};
51+
for (const pair of raw.split(',')) {
52+
const eq = pair.indexOf('=');
53+
if (eq === -1) {
54+
continue;
55+
}
56+
const key = pair.substring(0, eq).trim();
57+
const value = pair.substring(eq + 1).trim();
58+
if (key) {
59+
try {
60+
result[key] = decodeURIComponent(value);
61+
} catch {
62+
result[key] = value;
63+
}
64+
}
65+
}
66+
return result;
67+
}
68+
4269
/**
4370
* Returns a Resource for use in Sentry's OpenTelemetry TracerProvider setup.
4471
*
4572
* Combines the default OTel SDK telemetry attributes with Sentry-specific
4673
* service attributes, equivalent to what was previously done via:
4774
* `defaultResource().merge(resourceFromAttributes({ ... }))`
75+
*
76+
* Respects OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES environment variables
77+
* per the OpenTelemetry specification.
4878
*/
49-
export function getSentryResource(serviceName: string): SentryResource {
79+
export function getSentryResource(serviceNameFallback: string): SentryResource {
80+
const env = typeof process !== 'undefined' ? process.env : {};
81+
const otelServiceName = env.OTEL_SERVICE_NAME;
82+
const otelResourceAttrs = parseOtelResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES);
83+
5084
return new SentryResource({
51-
[ATTR_SERVICE_NAME]: serviceName,
85+
// Lowest priority: Sentry defaults
5286
// eslint-disable-next-line deprecation/deprecation
5387
[SEMRESATTRS_SERVICE_NAMESPACE]: 'sentry',
88+
[ATTR_SERVICE_NAME]: serviceNameFallback,
89+
// OTEL_RESOURCE_ATTRIBUTES overrides defaults (including service.name and service.namespace)
90+
...otelResourceAttrs,
91+
// OTEL_SERVICE_NAME explicitly overrides service.name
92+
...(otelServiceName ? { [ATTR_SERVICE_NAME]: otelServiceName } : {}),
93+
// Highest priority: Sentry SDK telemetry attrs (cannot be overridden by env vars)
5494
[ATTR_SERVICE_VERSION]: SDK_VERSION,
5595
[ATTR_TELEMETRY_SDK_LANGUAGE]: SDK_INFO[ATTR_TELEMETRY_SDK_LANGUAGE],
5696
[ATTR_TELEMETRY_SDK_NAME]: SDK_INFO[ATTR_TELEMETRY_SDK_NAME],
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
ATTR_SERVICE_NAME,
3+
ATTR_SERVICE_VERSION,
4+
ATTR_TELEMETRY_SDK_LANGUAGE,
5+
ATTR_TELEMETRY_SDK_NAME,
6+
ATTR_TELEMETRY_SDK_VERSION,
7+
SEMRESATTRS_SERVICE_NAMESPACE,
8+
} from '@opentelemetry/semantic-conventions';
9+
import { SDK_VERSION } from '@sentry/core';
10+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11+
import { getSentryResource } from '../src/resource';
12+
import { SDK_INFO } from '@opentelemetry/core';
13+
14+
describe('getSentryResource', () => {
15+
const originalEnv = process.env;
16+
17+
beforeEach(() => {
18+
// Clone env so mutations are isolated
19+
process.env = { ...originalEnv };
20+
delete process.env['OTEL_SERVICE_NAME'];
21+
delete process.env['OTEL_RESOURCE_ATTRIBUTES'];
22+
});
23+
24+
afterEach(() => {
25+
process.env = originalEnv;
26+
vi.restoreAllMocks();
27+
});
28+
29+
it('uses serviceNameFallback when no env vars are set', () => {
30+
const resource = getSentryResource('node');
31+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('node');
32+
});
33+
34+
it('uses OTEL_SERVICE_NAME over the fallback', () => {
35+
process.env['OTEL_SERVICE_NAME'] = 'my-service';
36+
const resource = getSentryResource('node');
37+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('my-service');
38+
});
39+
40+
it('ignores empty OTEL_SERVICE_NAME and falls back to serviceNameFallback', () => {
41+
process.env['OTEL_SERVICE_NAME'] = '';
42+
const resource = getSentryResource('node');
43+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('node');
44+
});
45+
46+
it('includes OTEL_RESOURCE_ATTRIBUTES key=value pairs', () => {
47+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'custom.key=custom-value,another.key=another-value';
48+
const resource = getSentryResource('node');
49+
expect(resource.attributes['custom.key']).toBe('custom-value');
50+
expect(resource.attributes['another.key']).toBe('another-value');
51+
});
52+
53+
it('OTEL_RESOURCE_ATTRIBUTES can override service.name (but OTEL_SERVICE_NAME takes precedence over it)', () => {
54+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.name=from-attrs';
55+
const resource = getSentryResource('node');
56+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('from-attrs');
57+
});
58+
59+
it('OTEL_SERVICE_NAME takes precedence over service.name from OTEL_RESOURCE_ATTRIBUTES', () => {
60+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.name=from-attrs';
61+
process.env['OTEL_SERVICE_NAME'] = 'from-service-name';
62+
const resource = getSentryResource('node');
63+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('from-service-name');
64+
});
65+
66+
it('OTEL_RESOURCE_ATTRIBUTES can override service.namespace', () => {
67+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.namespace=my-namespace';
68+
const resource = getSentryResource('node');
69+
// eslint-disable-next-line deprecation/deprecation
70+
expect(resource.attributes[SEMRESATTRS_SERVICE_NAMESPACE]).toBe('my-namespace');
71+
});
72+
73+
it('Sentry SDK telemetry attrs cannot be overridden by OTEL_RESOURCE_ATTRIBUTES', () => {
74+
process.env['OTEL_RESOURCE_ATTRIBUTES'] =
75+
'telemetry.sdk.name=evil,telemetry.sdk.language=evil,telemetry.sdk.version=0.0.0';
76+
const resource = getSentryResource('node');
77+
// not evil or 0.0.0
78+
expect(resource.attributes[ATTR_TELEMETRY_SDK_NAME]).toBe(SDK_INFO[ATTR_TELEMETRY_SDK_NAME]);
79+
expect(resource.attributes[ATTR_TELEMETRY_SDK_LANGUAGE]).toBe(SDK_INFO[ATTR_TELEMETRY_SDK_LANGUAGE]);
80+
expect(resource.attributes[ATTR_TELEMETRY_SDK_VERSION]).toBe(SDK_INFO[ATTR_TELEMETRY_SDK_VERSION]);
81+
});
82+
83+
it('Sentry SDK telemetry attrs cannot be overridden by OTEL_SERVICE_NAME (service.version)', () => {
84+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.version=0.0.0';
85+
const resource = getSentryResource('node');
86+
expect(resource.attributes[ATTR_SERVICE_VERSION]).toBe(SDK_VERSION);
87+
});
88+
89+
it('always includes Sentry SDK telemetry attributes', () => {
90+
const resource = getSentryResource('node');
91+
expect(resource.attributes[ATTR_TELEMETRY_SDK_LANGUAGE]).toBeDefined();
92+
expect(resource.attributes[ATTR_TELEMETRY_SDK_NAME]).toBeDefined();
93+
expect(resource.attributes[ATTR_TELEMETRY_SDK_VERSION]).toBeDefined();
94+
expect(resource.attributes[ATTR_SERVICE_VERSION]).toBe(SDK_VERSION);
95+
});
96+
97+
it('always sets service.namespace to sentry by default', () => {
98+
const resource = getSentryResource('node');
99+
// eslint-disable-next-line deprecation/deprecation
100+
expect(resource.attributes[SEMRESATTRS_SERVICE_NAMESPACE]).toBe('sentry');
101+
});
102+
103+
it('URL-decodes values in OTEL_RESOURCE_ATTRIBUTES', () => {
104+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'custom.key=hello%20world';
105+
const resource = getSentryResource('node');
106+
expect(resource.attributes['custom.key']).toBe('hello world');
107+
});
108+
109+
it('handles malformed OTEL_RESOURCE_ATTRIBUTES gracefully (no = sign)', () => {
110+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'badentry,custom.key=value';
111+
expect(() => getSentryResource('node')).not.toThrow();
112+
const resource = getSentryResource('node');
113+
expect(resource.attributes['custom.key']).toBe('value');
114+
});
115+
116+
it('handles empty OTEL_RESOURCE_ATTRIBUTES gracefully', () => {
117+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = '';
118+
expect(() => getSentryResource('node')).not.toThrow();
119+
});
120+
121+
it('does not crash when process is undefined', () => {
122+
const saved = global.process;
123+
// @ts-expect-error — simulating edge runtime where process may be undefined
124+
global.process = undefined;
125+
try {
126+
expect(() => getSentryResource('node')).not.toThrow();
127+
} finally {
128+
global.process = saved;
129+
}
130+
});
131+
});

0 commit comments

Comments
 (0)