You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 — one in its APM, one in Ferro.
Must be done without adding a runtime dependency on OpenTelemetry — the SDK's httpx-only footprint is a feature. Use runtime detection (no-op when OTel isn't installed).
Already shipped (do not re-implement)
Response-side trace_id surfacing landed with langchain-ferrolabsai 0.1.0:
FerroClient._request reads x-trace-id / x-request-id headers and merges them into the parsed body, so ChatCompletion.trace_id / .provider / .latency_ms / .cost_usd are reliably populated (ferrolabsai/client.py, ferrolabsai/types.py).
FerroAPIError.request_id is populated from x-request-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 — one in its APM, one in Ferro.Must be done without adding a runtime dependency on OpenTelemetry — the SDK's
httpx-only footprint is a feature. Use runtime detection (no-op when OTel isn't installed).Already shipped (do not re-implement)
Response-side
trace_idsurfacing landed withlangchain-ferrolabsai 0.1.0:FerroClient._requestreadsx-trace-id/x-request-idheaders and merges them into the parsed body, soChatCompletion.trace_id/.provider/.latency_ms/.cost_usdare reliably populated (ferrolabsai/client.py,ferrolabsai/types.py).FerroAPIError.request_idis populated fromx-request-id.This issue is the outbound propagation half.
In scope
ferrolabsai/observability/:propagation.py— runtime OTel detection; W3Ctraceparent+tracestateinjection.attrs.py—gen_ai.*attribute helpers for users instrumenting their own spans.FerroClient._requestandAsyncFerroClient._requestcallinject_headers(headers)before sending (no-op when no OTel SDK is loaded).docs/architecture.mdupdate.Non-goals
trace_idsurfacing — already shipped.ai-gateway-plugins.trace_id/provider/latency_msonChatCompletionChunk(streaming) — tracked in Surface trace_id / provider / latency_ms on ChatCompletionChunk #17.Acceptance criteria
ferrolabsai/observability/exposesinject_headers(dict) -> dictandpropagation_active() -> bool.inject_headersis a no-op whenopentelemetry.traceis not importable (test by monkey-patchingsys.modules).inject_headersadds a validtraceparentper W3C Trace Context._requestcallinject_headerson every outbound request.[project].dependencies; the OTel SDK appears only under test extras.mypy --strictpasses.pytest-httpxtests: notraceparentwhen OTel absent; matchingtraceparentwhen OTel + active span present.Implementation sketch (zero-dependency runtime detection)
Coordination
ai-gatewayv1.1.0traceparent(custom OTelIDGenerator) — SDK-injected traceparent becomes the root span. Nothing to wait on. Telemetry contract:ferro.observability.v1(docs/observability/schema.md).ferrolabs-typescript-sdktrace_id, TStraceId.ai-gateway-cookbook04-langsmith-tracingdemonstrates the end-to-end story once both SDKs ship this.ai-gateway-plugins(v1.2)observability/langsmith·langfuse·phoenixbridges are the consumer side of the joined trace.Test plan
New
tests/test_observability.py:test_inject_headers_no_op_when_otel_missingtest_inject_headers_uses_active_span_when_otel_loadedtest_async_client_propagates_traceparent