diff --git a/containers/api-proxy/otel-exporters.js b/containers/api-proxy/otel-exporters.js index 5ee9cde8..31a0cb9c 100644 --- a/containers/api-proxy/otel-exporters.js +++ b/containers/api-proxy/otel-exporters.js @@ -172,7 +172,51 @@ class FileSpanExporter { } } +/** + * Fan-out exporter that sends spans to multiple OTLP endpoints concurrently. + * Partial failures on individual endpoints do not block export to others. + */ +class FanOutSpanExporter { + /** + * @param {ProxyAwareOtlpExporter[]} exporters - Array of per-endpoint exporters + */ + constructor(exporters) { + this._exporters = exporters; + } + + export(spans, resultCallback) { + if (!spans || spans.length === 0 || this._exporters.length === 0) { + resultCallback({ code: 0 }); + return; + } + + let pending = this._exporters.length; + let anySuccess = false; + + const onDone = (result) => { + if (result.code === 0) anySuccess = true; + pending--; + if (pending === 0) { + resultCallback({ code: anySuccess ? 0 : 1 }); + } + }; + + for (const exporter of this._exporters) { + try { + exporter.export(spans, onDone); + } catch { + onDone({ code: 1 }); + } + } + } + + shutdown() { + return Promise.all(this._exporters.map(e => e.shutdown())); + } +} + module.exports = { ProxyAwareOtlpExporter, FileSpanExporter, + FanOutSpanExporter, }; diff --git a/containers/api-proxy/otel-fanout.test.js b/containers/api-proxy/otel-fanout.test.js new file mode 100644 index 00000000..4aa51a8d --- /dev/null +++ b/containers/api-proxy/otel-fanout.test.js @@ -0,0 +1,279 @@ +'use strict'; + +/** + * Tests for OTLP fan-out exporter and GH_AW_OTLP_ENDPOINTS parsing. + */ + +const { FanOutSpanExporter } = require('./otel-exporters'); + +// ── FanOutSpanExporter unit tests ───────────────────────────────────────────── + +describe('FanOutSpanExporter', () => { + function makeMockExporter(resultCode = 0) { + const calls = []; + return { + calls, + export(spans, cb) { + calls.push(spans); + cb({ code: resultCode }); + }, + shutdown() { return Promise.resolve(); }, + }; + } + + test('exports spans to all child exporters', (done) => { + const e1 = makeMockExporter(0); + const e2 = makeMockExporter(0); + const fanout = new FanOutSpanExporter([e1, e2]); + + const fakeSpans = [{ name: 'test-span' }]; + fanout.export(fakeSpans, (result) => { + expect(result.code).toBe(0); + expect(e1.calls).toHaveLength(1); + expect(e2.calls).toHaveLength(1); + expect(e1.calls[0]).toBe(fakeSpans); + done(); + }); + }); + + test('succeeds if at least one exporter succeeds', (done) => { + const e1 = makeMockExporter(1); // fails + const e2 = makeMockExporter(0); // succeeds + const fanout = new FanOutSpanExporter([e1, e2]); + + fanout.export([{ name: 'span' }], (result) => { + expect(result.code).toBe(0); // partial success = success + done(); + }); + }); + + test('fails only when all exporters fail', (done) => { + const e1 = makeMockExporter(1); + const e2 = makeMockExporter(1); + const fanout = new FanOutSpanExporter([e1, e2]); + + fanout.export([{ name: 'span' }], (result) => { + expect(result.code).toBe(1); + done(); + }); + }); + + test('handles empty spans gracefully', (done) => { + const e1 = makeMockExporter(0); + const fanout = new FanOutSpanExporter([e1]); + + fanout.export([], (result) => { + expect(result.code).toBe(0); + expect(e1.calls).toHaveLength(0); + done(); + }); + }); + + test('handles exporter throwing', (done) => { + const throwing = { + export() { throw new Error('boom'); }, + shutdown() { return Promise.resolve(); }, + }; + const good = makeMockExporter(0); + const fanout = new FanOutSpanExporter([throwing, good]); + + fanout.export([{ name: 'span' }], (result) => { + expect(result.code).toBe(0); // good exporter succeeded + expect(good.calls).toHaveLength(1); + done(); + }); + }); + + test('shutdown calls all child exporters', async () => { + const shutdowns = []; + const e1 = { export() {}, shutdown() { shutdowns.push(1); return Promise.resolve(); } }; + const e2 = { export() {}, shutdown() { shutdowns.push(2); return Promise.resolve(); } }; + const fanout = new FanOutSpanExporter([e1, e2]); + + await fanout.shutdown(); + expect(shutdowns).toEqual([1, 2]); + }); +}); + +// ── _parseEndpoints unit tests ──────────────────────────────────────────────── + +describe('_parseEndpoints', () => { + function loadOtelFresh(envOverrides = {}) { + const saved = {}; + const keys = [ + 'GH_AW_OTLP_ENDPOINTS', + 'OTEL_EXPORTER_OTLP_ENDPOINT', + 'OTEL_EXPORTER_OTLP_HEADERS', + 'OTEL_SERVICE_NAME', + 'GITHUB_AW_OTEL_TRACE_ID', + 'GITHUB_AW_OTEL_PARENT_SPAN_ID', + 'HTTPS_PROXY', + 'HTTP_PROXY', + 'AWF_VERSION', + ]; + for (const k of keys) { + saved[k] = process.env[k]; + delete process.env[k]; + } + Object.assign(process.env, envOverrides); + + jest.resetModules(); + const mod = require('./otel'); + + for (const k of keys) { + if (saved[k] !== undefined) process.env[k] = saved[k]; + else delete process.env[k]; + } + return mod; + } + + test('returns empty array when env var is absent', () => { + const otel = loadOtelFresh({}); + expect(otel._parseEndpoints()).toEqual([]); + }); + + test('parses valid JSON array of endpoints', () => { + const endpoints = [ + { url: 'https://primary.example.com:4318', headers: { 'Authorization': 'Bearer abc' } }, + { url: 'https://secondary.example.com:4318', headers: { 'X-Api-Key': 'xyz' } }, + ]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel._parseEndpoints()).toEqual(endpoints); + }); + + test('filters out entries without url', () => { + const endpoints = [ + { url: 'https://valid.example.com', headers: {} }, + { headers: { 'X-Key': 'val' } }, + { url: '', headers: {} }, + null, + ]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel._parseEndpoints()).toEqual([ + { url: 'https://valid.example.com', headers: {} }, + ]); + }); + + test('returns empty array for invalid JSON', () => { + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: 'not-json{' }); + expect(otel._parseEndpoints()).toEqual([]); + }); + + test('returns empty array for non-array JSON', () => { + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: '{"url": "https://x.com"}' }); + expect(otel._parseEndpoints()).toEqual([]); + }); + + test('filters out entries with invalid URLs', () => { + const endpoints = [ + { url: 'https://valid.example.com', headers: {} }, + { url: 'not-a-valid-url', headers: {} }, + { url: '/relative/path', headers: {} }, + ]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel._parseEndpoints()).toEqual([ + { url: 'https://valid.example.com', headers: {} }, + ]); + }); + + test('normalizes array headers to empty object', () => { + const endpoints = [{ url: 'https://array-headers.example.com', headers: ['Authorization', '******'] }]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel._parseEndpoints()).toEqual([ + { url: 'https://array-headers.example.com', headers: {} }, + ]); + }); + + test('filters out non-string header values', () => { + const endpoints = [{ + url: 'https://mixed-headers.example.com', + headers: { 'Authorization': '******', 'X-Count': 42, 'X-Flag': true, 'X-Valid': 'yes' }, + }]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel._parseEndpoints()).toEqual([ + { url: 'https://mixed-headers.example.com', headers: { 'Authorization': '******', 'X-Valid': 'yes' } }, + ]); + }); + + + test('normalizes missing headers to empty object', () => { + const endpoints = [{ url: 'https://no-headers.example.com' }]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel._parseEndpoints()).toEqual([ + { url: 'https://no-headers.example.com', headers: {} }, + ]); + }); +}); + +// ── Integration: fan-out initialization ─────────────────────────────────────── + +describe('otel fan-out initialization', () => { + function loadOtelFresh(envOverrides = {}) { + const saved = {}; + const keys = [ + 'GH_AW_OTLP_ENDPOINTS', + 'OTEL_EXPORTER_OTLP_ENDPOINT', + 'OTEL_EXPORTER_OTLP_HEADERS', + 'OTEL_SERVICE_NAME', + 'GITHUB_AW_OTEL_TRACE_ID', + 'GITHUB_AW_OTEL_PARENT_SPAN_ID', + 'HTTPS_PROXY', + 'HTTP_PROXY', + 'AWF_VERSION', + ]; + for (const k of keys) { + saved[k] = process.env[k]; + delete process.env[k]; + } + Object.assign(process.env, envOverrides); + + jest.resetModules(); + const mod = require('./otel'); + + for (const k of keys) { + if (saved[k] !== undefined) process.env[k] = saved[k]; + else delete process.env[k]; + } + return mod; + } + + test('initializes with FanOutSpanExporter when multiple endpoints configured', () => { + const endpoints = [ + { url: 'https://a.example.com:4318' }, + { url: 'https://b.example.com:4318' }, + ]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel.isEnabled()).toBe(true); + // Provider should be initialized + expect(otel._provider).not.toBeNull(); + }); + + test('initializes with single ProxyAwareOtlpExporter for one endpoint in array', () => { + const endpoints = [{ url: 'https://single.example.com:4318' }]; + const otel = loadOtelFresh({ GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints) }); + expect(otel.isEnabled()).toBe(true); + }); + + test('falls back to OTEL_EXPORTER_OTLP_ENDPOINT when GH_AW_OTLP_ENDPOINTS absent', () => { + const otel = loadOtelFresh({ OTEL_EXPORTER_OTLP_ENDPOINT: 'https://legacy.example.com' }); + expect(otel.isEnabled()).toBe(true); + }); + + test('GH_AW_OTLP_ENDPOINTS takes priority over OTEL_EXPORTER_OTLP_ENDPOINT', () => { + const endpoints = [ + { url: 'https://fanout1.example.com' }, + { url: 'https://fanout2.example.com' }, + ]; + const otel = loadOtelFresh({ + GH_AW_OTLP_ENDPOINTS: JSON.stringify(endpoints), + OTEL_EXPORTER_OTLP_ENDPOINT: 'https://legacy.example.com', + }); + expect(otel.isEnabled()).toBe(true); + }); + + test('uses FileSpanExporter when no OTLP config at all', () => { + const otel = loadOtelFresh({}); + expect(otel.isEnabled()).toBe(true); + // Still enabled, just writes to file + }); +}); diff --git a/containers/api-proxy/otel.js b/containers/api-proxy/otel.js index b51a965c..829f512e 100644 --- a/containers/api-proxy/otel.js +++ b/containers/api-proxy/otel.js @@ -9,18 +9,20 @@ * GITHUB_AW_OTEL_PARENT_SPAN_ID so that end-to-end traces flow from the * GitHub Actions workflow through the api-proxy to the LLM provider. * - * Activation: - * - When OTEL_EXPORTER_OTLP_ENDPOINT is set: exports via OTLP/HTTP routed - * through the Squid proxy (HTTPS_PROXY / HTTP_PROXY env vars) so the - * domain whitelist is respected. - * - Otherwise: writes span NDJSON to /var/log/api-proxy/otel.jsonl as a + * Activation (in priority order): + * - GH_AW_OTLP_ENDPOINTS (JSON array of {url, headers} objects): fan-out + * mode — spans are exported concurrently to all listed endpoints. + * - OTEL_EXPORTER_OTLP_ENDPOINT (single URL): legacy single-endpoint mode. + * - Neither set: writes span NDJSON to /var/log/api-proxy/otel.jsonl as a * local fallback (mirrors the MCPG /tmp/gh-aw/otel.jsonl pattern). - * - Network export remains opt-in. When OTLP is unset, only best-effort - * local file writes are attempted (no outbound network traffic). + * + * All OTLP exports route through the Squid proxy (HTTPS_PROXY / HTTP_PROXY) + * so the domain whitelist is respected. * * Environment variables consumed: - * OTEL_EXPORTER_OTLP_ENDPOINT - OTLP/HTTP collector URL (e.g. https://otel.example.com:4318) - * OTEL_EXPORTER_OTLP_HEADERS - Comma-separated "key=value" auth headers + * GH_AW_OTLP_ENDPOINTS - JSON array of {url, headers} endpoint objects (fan-out) + * OTEL_EXPORTER_OTLP_ENDPOINT - OTLP/HTTP collector URL (single-endpoint fallback) + * OTEL_EXPORTER_OTLP_HEADERS - Comma-separated "key=value" auth headers (single-endpoint) * OTEL_SERVICE_NAME - Service name tag (default: awf-api-proxy) * GITHUB_AW_OTEL_TRACE_ID - W3C trace-id of the parent workflow trace * GITHUB_AW_OTEL_PARENT_SPAN_ID - W3C span-id of the parent workflow span @@ -43,9 +45,10 @@ const { INVALID_SPAN_CONTEXT, } = require('@opentelemetry/api'); const { parseOtlpHeaders, buildResourceSpans } = require('./otel-serialization'); -const { ProxyAwareOtlpExporter, FileSpanExporter } = require('./otel-exporters'); +const { ProxyAwareOtlpExporter, FileSpanExporter, FanOutSpanExporter } = require('./otel-exporters'); // ── Environment variables ───────────────────────────────────────────────────── +const OTLP_ENDPOINTS_JSON = (process.env.GH_AW_OTLP_ENDPOINTS || '').trim(); const OTLP_ENDPOINT = (process.env.OTEL_EXPORTER_OTLP_ENDPOINT || '').trim(); const OTLP_HEADERS_RAW = (process.env.OTEL_EXPORTER_OTLP_HEADERS || '').trim(); const SERVICE_NAME = (process.env.OTEL_SERVICE_NAME || 'awf-api-proxy').trim(); @@ -63,11 +66,66 @@ let _enabled = false; // ── SDK initialisation ──────────────────────────────────────────────────────── +/** + * Parse GH_AW_OTLP_ENDPOINTS JSON array into [{url, headers}] objects. + * Returns empty array if the env var is absent/invalid. + * Each entry: { url: string, headers: Record } + * + * URLs are validated with `new URL()` up-front so that a misconfigured entry + * does not cause ProxyAwareOtlpExporter construction to throw during module + * init. Headers are normalized to a plain string-to-string object; arrays and + * entries with non-string values are silently dropped. + */ +function _parseEndpoints() { + if (!OTLP_ENDPOINTS_JSON) return []; + try { + const parsed = JSON.parse(OTLP_ENDPOINTS_JSON); + if (!Array.isArray(parsed)) return []; + const results = []; + for (const ep of parsed) { + if (!ep || typeof ep.url !== 'string') continue; + const urlStr = ep.url.trim(); + if (!urlStr) continue; + try { new URL(urlStr); } catch { continue; } + const rawHeaders = ep.headers; + const headers = {}; + if (rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) { + for (const [k, v] of Object.entries(rawHeaders)) { + if (typeof v === 'string') headers[k] = v; + } + } + results.push({ url: urlStr, headers }); + } + return results; + } catch { + return []; + } +} + function _init() { const resource = new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }); let exporter; - if (OTLP_ENDPOINT) { + const endpoints = _parseEndpoints(); + + if (endpoints.length > 1) { + // Fan-out: send spans to all configured endpoints concurrently + const exporters = endpoints.map(ep => new ProxyAwareOtlpExporter({ + url: ep.url, + headers: ep.headers || {}, + httpsProxy: HTTPS_PROXY_URL || null, + resource, + })); + exporter = new FanOutSpanExporter(exporters); + } else if (endpoints.length === 1) { + exporter = new ProxyAwareOtlpExporter({ + url: endpoints[0].url, + headers: endpoints[0].headers || {}, + httpsProxy: HTTPS_PROXY_URL || null, + resource, + }); + } else if (OTLP_ENDPOINT) { + // Legacy single-endpoint fallback exporter = new ProxyAwareOtlpExporter({ url: OTLP_ENDPOINT, headers: parseOtlpHeaders(OTLP_HEADERS_RAW), @@ -299,6 +357,8 @@ module.exports = { get _provider() { return _provider; }, _ProxyAwareOtlpExporter: ProxyAwareOtlpExporter, _FileSpanExporter: FileSpanExporter, + _FanOutSpanExporter: FanOutSpanExporter, + _parseEndpoints, _parseOtlpHeaders: parseOtlpHeaders, _buildResourceSpans: buildResourceSpans, }; diff --git a/docs/awf-config-spec.md b/docs/awf-config-spec.md index 638cbb4e..085d3957 100644 --- a/docs/awf-config-spec.md +++ b/docs/awf-config-spec.md @@ -364,8 +364,9 @@ When the API proxy sidecar is enabled, the following rules apply: | Variable | Description | |----------|-------------| - | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP/HTTP collector URL. When present, activates span export via Squid proxy. | - | `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` auth headers for the OTLP endpoint. | + | `GH_AW_OTLP_ENDPOINTS` | JSON array of `{url, headers}` objects for fan-out export to multiple OTLP collectors. Takes priority over `OTEL_EXPORTER_OTLP_ENDPOINT`. | + | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP/HTTP collector URL. Single-endpoint fallback when `GH_AW_OTLP_ENDPOINTS` is absent. | + | `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` auth headers for the OTLP endpoint. Only used with `OTEL_EXPORTER_OTLP_ENDPOINT`. | | `OTEL_SERVICE_NAME` | Service name tag. Defaults to `awf-api-proxy` when not set. | | `GITHUB_AW_OTEL_TRACE_ID` | W3C trace-id of the parent workflow trace. | | `GITHUB_AW_OTEL_PARENT_SPAN_ID` | W3C span-id of the parent workflow span. | @@ -374,8 +375,15 @@ When the API proxy sidecar is enabled, the following rules apply: the agent receives OTEL variables through the standard `OTEL_*` prefix forwarding described in §8.4. - When `OTEL_EXPORTER_OTLP_ENDPOINT` is absent, the sidecar writes span - NDJSON to `/var/log/api-proxy/otel.jsonl` as a local fallback. + The sidecar selects its exporter using the following priority order: + + 1. `GH_AW_OTLP_ENDPOINTS` (JSON array) — spans are exported concurrently to + all listed endpoints (fan-out mode); partial failures on individual + endpoints do not block others. + 2. `OTEL_EXPORTER_OTLP_ENDPOINT` (single URL) — legacy single-endpoint mode. + 3. Neither set — the sidecar writes span NDJSON to + `/var/log/api-proxy/otel.jsonl` as a local fallback. + When `GITHUB_AW_OTEL_TRACE_ID` / `GITHUB_AW_OTEL_PARENT_SPAN_ID` are present and valid hex, each sidecar span is created as a child of the specified parent span, enabling end-to-end distributed tracing from the diff --git a/package-lock.json b/package-lock.json index 46c65212..26b390a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", "babel-jest": "^30.2.0", - "esbuild": "^0.25.0", + "esbuild": "^0.28.1", "eslint": "^10.2.1", "eslint-plugin-security": "^3.0.1", "glob": "^13.0.1", @@ -2136,9 +2136,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -2153,9 +2153,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -2170,9 +2170,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -2187,9 +2187,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -2204,9 +2204,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -2221,9 +2221,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -2238,9 +2238,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -2255,9 +2255,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -2272,9 +2272,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -2289,9 +2289,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -2306,9 +2306,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -2323,9 +2323,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -2340,9 +2340,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -2357,9 +2357,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -2374,9 +2374,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -2391,9 +2391,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -2408,9 +2408,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -2425,9 +2425,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -2442,9 +2442,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -2459,9 +2459,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -2476,9 +2476,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -2493,9 +2493,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -2510,9 +2510,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -2527,9 +2527,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -2544,9 +2544,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -2561,9 +2561,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -5089,9 +5089,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5102,32 +5102,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { diff --git a/package.json b/package.json index c83b50f4..976c73a2 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", "babel-jest": "^30.2.0", - "esbuild": "^0.25.0", + "esbuild": "^0.28.1", "eslint": "^10.2.1", "eslint-plugin-security": "^3.0.1", "glob": "^13.0.1", diff --git a/src/services/api-proxy-service-config.ts b/src/services/api-proxy-service-config.ts index bae8a4aa..368fff76 100644 --- a/src/services/api-proxy-service-config.ts +++ b/src/services/api-proxy-service-config.ts @@ -131,9 +131,11 @@ export function buildApiProxyServiceConfig(params: ApiProxyServiceConfigParams): no_proxy: 'localhost,127.0.0.1,::1', // OpenTelemetry distributed tracing — forward endpoint, headers, service name, and // parent trace context so api-proxy spans are children of the workflow trace. - // OTEL_EXPORTER_OTLP_ENDPOINT activates OTLP/HTTP export (via Squid); when absent - // spans are written to /var/log/api-proxy/otel.jsonl as a local file fallback. + // GH_AW_OTLP_ENDPOINTS (JSON array) enables fan-out to multiple collectors. + // OTEL_EXPORTER_OTLP_ENDPOINT is kept for backward compat (single-endpoint fallback). + // When neither is set, spans are written to /var/log/api-proxy/otel.jsonl. ...pickEnvVars( + 'GH_AW_OTLP_ENDPOINTS', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_HEADERS', 'GITHUB_AW_OTEL_TRACE_ID',