diff --git a/.changeset/moody-avocados-allow.md b/.changeset/moody-avocados-allow.md new file mode 100644 index 00000000..509fd4b8 --- /dev/null +++ b/.changeset/moody-avocados-allow.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/node-opentelemetry': patch +--- + +Fix issue where OTEL_EXPORTER_OTLP_HEADERS are not passed to health check endpoints diff --git a/packages/node-opentelemetry/src/__tests__/parseHeaders.ts b/packages/node-opentelemetry/src/__tests__/parseHeaders.ts new file mode 100644 index 00000000..df9506cd --- /dev/null +++ b/packages/node-opentelemetry/src/__tests__/parseHeaders.ts @@ -0,0 +1,71 @@ +import { parseOtlpHeaders } from '../utils'; + +describe('Parse OTLP Headers', () => { + it('should return an empty object when no headers string is provided', () => { + expect(parseOtlpHeaders()).toEqual({}); + }); + + it('should correctly parse a single header', () => { + expect(parseOtlpHeaders('key1=value1')).toEqual({ key1: 'value1' }); + }); + + it('should correctly parse multiple headers', () => { + expect(parseOtlpHeaders('key1=value1,key2=value2')).toEqual({ + key1: 'value1', + key2: 'value2', + }); + }); + + it('should handle spaces around keys and values', () => { + expect(parseOtlpHeaders(' key1 = value1 , key2 = value2 ')).toEqual({ + key1: 'value1', + key2: 'value2', + }); + }); + + it('should ignore malformed headers without "="', () => { + expect(parseOtlpHeaders('key1=value1,malformedHeader,key2=value2')).toEqual( + { + key1: 'value1', + key2: 'value2', + }, + ); + }); + + it('should handle empty values', () => { + expect(parseOtlpHeaders('key1=,key2=value2')).toEqual({ + key1: '', + key2: 'value2', + }); + }); + + it('should handle special characters in values', () => { + expect( + parseOtlpHeaders( + 'Authorization=Bearer token123,Content-Type=application/json', + ), + ).toEqual({ + Authorization: 'Bearer token123', + 'Content-Type': 'application/json', + }); + }); + + it('should handle trailing comma', () => { + expect(parseOtlpHeaders('key1=value1,')).toEqual({ + key1: 'value1', + }); + }); + + it('should handle leading comma', () => { + expect(parseOtlpHeaders(',key1=value1')).toEqual({ + key1: 'value1', + }); + }); + + it('should handle multiple consecutive commas', () => { + expect(parseOtlpHeaders('key1=value1,,key2=value2')).toEqual({ + key1: 'value1', + key2: 'value2', + }); + }); +}); diff --git a/packages/node-opentelemetry/src/otel.ts b/packages/node-opentelemetry/src/otel.ts index aaa4d93e..ce946192 100644 --- a/packages/node-opentelemetry/src/otel.ts +++ b/packages/node-opentelemetry/src/otel.ts @@ -49,6 +49,7 @@ import { getHyperDXMetricReader } from './metrics'; import { MutableAsyncLocalStorageContextManager } from './MutableAsyncLocalStorageContextManager'; import { Logger as OtelLogger } from './otel-logger'; import HyperDXSpanProcessor from './spanProcessor'; +import { parseOtlpHeaders } from './utils'; const UI_LOG_PREFIX = '[⚡HyperDX]'; @@ -215,6 +216,13 @@ export const initSDK = (config: SDKConfig) => { }); ui.succeed('Set default otel envs'); + // Parse OTLP headers from environment variable + const otlpHeaders = parseOtlpHeaders(env.OTEL_EXPORTER_OTLP_HEADERS); + const healthCheckHeaders = { + 'Content-Type': 'application/json', + ...otlpHeaders, + }; + const stopOnTerminationSignals = config.stopOnTerminationSignals ?? DEFAULT_HDX_NODE_STOP_ON_TERMINATION_SIGNALS; // Stop by default @@ -244,23 +252,17 @@ export const initSDK = (config: SDKConfig) => { Promise.all([ healthCheckUrl(ui, DEFAULT_OTEL_TRACES_EXPORTER_URL, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: healthCheckHeaders, body: JSON.stringify({}), }), healthCheckUrl(ui, _logger.getExporterUrl(), { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: healthCheckHeaders, body: JSON.stringify({}), }), healthCheckUrl(ui, DEFAULT_OTEL_METRICS_EXPORTER_URL, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: healthCheckHeaders, body: JSON.stringify({}), }), ]); diff --git a/packages/node-opentelemetry/src/utils.ts b/packages/node-opentelemetry/src/utils.ts index f7c22096..2e4e5225 100644 --- a/packages/node-opentelemetry/src/utils.ts +++ b/packages/node-opentelemetry/src/utils.ts @@ -26,3 +26,40 @@ export const stringToBoolean = (stringValue: string | undefined) => { return undefined; } }; + +/** + * Parses OTEL_EXPORTER_OTLP_HEADERS environment variable into a structured headers object. + * Format: "key1=value1,key2=value2" -> { key1: "value1", key2: "value2" } + */ +export const parseOtlpHeaders = ( + headersString?: string, +): Record => { + if (!headersString) { + return {}; + } + + const headers: Record = {}; + const pairs = headersString.split(','); + + for (const pair of pairs) { + const trimmedPair = pair.trim(); + if (!trimmedPair) { + continue; + } + + const equalIndex = trimmedPair.indexOf('='); + if (equalIndex === -1) { + // Skip malformed pairs without '=' + continue; + } + + const key = trimmedPair.substring(0, equalIndex).trim(); + const value = trimmedPair.substring(equalIndex + 1).trim(); + + if (key) { + headers[key] = value; + } + } + + return headers; +};