Inject a W3C traceparent header on every outbound request so, when the calling app already runs OpenTelemetry with an active span, the gateway's root span becomes a child of the caller's span. Without this, an instrumented app sees two disconnected traces for one logical request.
Must be done without adding @opentelemetry/* to dependencies — the SDK stays tree-shakeable and edge-runtime-friendly. OTel comes from peerDependenciesMeta (optional: true); detect it at runtime via dynamic import().
Already shipped (do not re-implement)
Response-side surfacing exists: ChatCompletion carries trace_id / provider / latency_ms, and usage carries cost_usd / cache_hit / provider (see README "Observability"). This issue is the outbound propagation half only.
In scope
- New module
src/observability/:
propagation.ts — runtime detection of @opentelemetry/api; W3C traceparent (+ tracestate) injection.
attrs.ts — gen_ai.* attribute key constants for users instrumenting their own spans.
HttpClient (src/_internal/http.ts) calls injectHeaders(headers) on every outbound fetch — no-op when @opentelemetry/api isn't installed.
- README "Observability and tracing" propagation example +
examples/observability.ts.
Non-goals
- ❌ Re-implementing response-side surfacing — already shipped.
- ❌ Embedding vendor SDKs (LangSmith/Langfuse/Datadog) — those are gateway plugins in
ai-gateway-plugins.
- ❌ Auto-creating client-side spans — users wrap calls themselves (doc shows how).
Acceptance criteria
Implementation sketch (zero-dependency runtime detection)
let cachedApi: typeof import("@opentelemetry/api") | null | undefined;
async function loadOtel() {
if (cachedApi !== undefined) return cachedApi;
try { cachedApi = await import("@opentelemetry/api"); } catch { cachedApi = null; }
return cachedApi;
}
export async function injectHeaders(headers: Record<string, string>): Promise<Record<string, string>> {
const otel = await loadOtel();
if (!otel) return headers;
otel.propagation.inject(otel.context.active(), headers);
return headers;
}
Mirror of ferrolabs-python-sdk#20 (Python trace_id ↔ TS trace_id).
Inject a W3C
traceparentheader on every outbound request so, when the calling app already runs OpenTelemetry with an active span, the gateway's root span becomes a child of the caller's span. Without this, an instrumented app sees two disconnected traces for one logical request.Must be done without adding
@opentelemetry/*todependencies— the SDK stays tree-shakeable and edge-runtime-friendly. OTel comes frompeerDependenciesMeta(optional: true); detect it at runtime via dynamicimport().Already shipped (do not re-implement)
Response-side surfacing exists:
ChatCompletioncarriestrace_id/provider/latency_ms, andusagecarriescost_usd/cache_hit/provider(see README "Observability"). This issue is the outbound propagation half only.In scope
src/observability/:propagation.ts— runtime detection of@opentelemetry/api; W3Ctraceparent(+tracestate) injection.attrs.ts—gen_ai.*attribute key constants for users instrumenting their own spans.HttpClient(src/_internal/http.ts) callsinjectHeaders(headers)on every outbound fetch — no-op when@opentelemetry/apiisn't installed.examples/observability.ts.Non-goals
ai-gateway-plugins.Acceptance criteria
src/observability/exportsinjectHeaders(headers: Record<string,string>): Record<string,string>andpropagationActive(): boolean.injectHeadersreturns input unchanged when@opentelemetry/apiis not importable at runtime (test via Vitestvi.doMock).traceparentis added.HttpClientcallsinjectHeaderson every outbound request.dependencies;@opentelemetry/apionly inpeerDependenciesMeta(optional: true). Package keeps"sideEffects": false.tsc --noEmit, ESLint, Prettier all clean.injectHeadersdoes not throw whenprocessis undefined.Implementation sketch (zero-dependency runtime detection)
Mirror of
ferrolabs-python-sdk#20(Pythontrace_id↔ TStrace_id).