Skip to content

Commit d056014

Browse files
committed
fix(opentelemetry): Respect OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES
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 b045541 commit d056014

2 files changed

Lines changed: 171 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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
13+
describe('getSentryResource', () => {
14+
const originalEnv = process.env;
15+
16+
beforeEach(() => {
17+
// Clone env so mutations are isolated
18+
process.env = { ...originalEnv };
19+
delete process.env['OTEL_SERVICE_NAME'];
20+
delete process.env['OTEL_RESOURCE_ATTRIBUTES'];
21+
});
22+
23+
afterEach(() => {
24+
process.env = originalEnv;
25+
vi.restoreAllMocks();
26+
});
27+
28+
it('uses serviceNameFallback when no env vars are set', () => {
29+
const resource = getSentryResource('node');
30+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('node');
31+
});
32+
33+
it('uses OTEL_SERVICE_NAME over the fallback', () => {
34+
process.env['OTEL_SERVICE_NAME'] = 'my-service';
35+
const resource = getSentryResource('node');
36+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('my-service');
37+
});
38+
39+
it('ignores empty OTEL_SERVICE_NAME and falls back to serviceNameFallback', () => {
40+
process.env['OTEL_SERVICE_NAME'] = '';
41+
const resource = getSentryResource('node');
42+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('node');
43+
});
44+
45+
it('includes OTEL_RESOURCE_ATTRIBUTES key=value pairs', () => {
46+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'custom.key=custom-value,another.key=another-value';
47+
const resource = getSentryResource('node');
48+
expect(resource.attributes['custom.key']).toBe('custom-value');
49+
expect(resource.attributes['another.key']).toBe('another-value');
50+
});
51+
52+
it('OTEL_RESOURCE_ATTRIBUTES can override service.name (but OTEL_SERVICE_NAME takes precedence over it)', () => {
53+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.name=from-attrs';
54+
const resource = getSentryResource('node');
55+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('from-attrs');
56+
});
57+
58+
it('OTEL_SERVICE_NAME takes precedence over service.name from OTEL_RESOURCE_ATTRIBUTES', () => {
59+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.name=from-attrs';
60+
process.env['OTEL_SERVICE_NAME'] = 'from-service-name';
61+
const resource = getSentryResource('node');
62+
expect(resource.attributes[ATTR_SERVICE_NAME]).toBe('from-service-name');
63+
});
64+
65+
it('OTEL_RESOURCE_ATTRIBUTES can override service.namespace', () => {
66+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.namespace=my-namespace';
67+
const resource = getSentryResource('node');
68+
// eslint-disable-next-line deprecation/deprecation
69+
expect(resource.attributes[SEMRESATTRS_SERVICE_NAMESPACE]).toBe('my-namespace');
70+
});
71+
72+
it('Sentry SDK telemetry attrs cannot be overridden by OTEL_RESOURCE_ATTRIBUTES', () => {
73+
process.env['OTEL_RESOURCE_ATTRIBUTES'] =
74+
'telemetry.sdk.name=evil,telemetry.sdk.language=evil,telemetry.sdk.version=0.0.0';
75+
const resource = getSentryResource('node');
76+
expect(resource.attributes[ATTR_TELEMETRY_SDK_NAME]).not.toBe('evil');
77+
expect(resource.attributes[ATTR_TELEMETRY_SDK_LANGUAGE]).not.toBe('evil');
78+
expect(resource.attributes[ATTR_TELEMETRY_SDK_VERSION]).not.toBe('0.0.0');
79+
});
80+
81+
it('Sentry SDK telemetry attrs cannot be overridden by OTEL_SERVICE_NAME (service.version)', () => {
82+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'service.version=0.0.0';
83+
const resource = getSentryResource('node');
84+
expect(resource.attributes[ATTR_SERVICE_VERSION]).toBe(SDK_VERSION);
85+
});
86+
87+
it('always includes Sentry SDK telemetry attributes', () => {
88+
const resource = getSentryResource('node');
89+
expect(resource.attributes[ATTR_TELEMETRY_SDK_LANGUAGE]).toBeDefined();
90+
expect(resource.attributes[ATTR_TELEMETRY_SDK_NAME]).toBeDefined();
91+
expect(resource.attributes[ATTR_TELEMETRY_SDK_VERSION]).toBeDefined();
92+
expect(resource.attributes[ATTR_SERVICE_VERSION]).toBe(SDK_VERSION);
93+
});
94+
95+
it('always sets service.namespace to sentry by default', () => {
96+
const resource = getSentryResource('node');
97+
// eslint-disable-next-line deprecation/deprecation
98+
expect(resource.attributes[SEMRESATTRS_SERVICE_NAMESPACE]).toBe('sentry');
99+
});
100+
101+
it('URL-decodes values in OTEL_RESOURCE_ATTRIBUTES', () => {
102+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'custom.key=hello%20world';
103+
const resource = getSentryResource('node');
104+
expect(resource.attributes['custom.key']).toBe('hello world');
105+
});
106+
107+
it('handles malformed OTEL_RESOURCE_ATTRIBUTES gracefully (no = sign)', () => {
108+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = 'badentry,custom.key=value';
109+
expect(() => getSentryResource('node')).not.toThrow();
110+
const resource = getSentryResource('node');
111+
expect(resource.attributes['custom.key']).toBe('value');
112+
});
113+
114+
it('handles empty OTEL_RESOURCE_ATTRIBUTES gracefully', () => {
115+
process.env['OTEL_RESOURCE_ATTRIBUTES'] = '';
116+
expect(() => getSentryResource('node')).not.toThrow();
117+
});
118+
119+
it('does not crash when process is undefined', () => {
120+
const saved = global.process;
121+
// @ts-expect-error — simulating edge runtime where process may be undefined
122+
global.process = undefined;
123+
try {
124+
expect(() => getSentryResource('node')).not.toThrow();
125+
} finally {
126+
global.process = saved;
127+
}
128+
});
129+
});

0 commit comments

Comments
 (0)