This repository was archived by the owner on Oct 1, 2026. It is now read-only.
Repository navigation
AAPP-1159: Add OTLP traces backend with dual-write to DO Traces API - #45
Merged
Merged
Conversation
Wire OpenTelemetry GenAI-conventioned spans out of gradient-adk to a
local OTLP collector while preserving the existing decorator API and
the X-Gradient-Trace-Id header. Traces are emitted via OTLP/HTTP-proto
to OTEL_EXPORTER_OTLP_ENDPOINT (default localhost:4318). Dual-write to
the legacy DO Traces API runs behind two env-var flippers
(DIGITALOCEAN_GALILEO_WRITE_ENABLED / DIGITALOCEAN_OTLP_WRITE_ENABLED)
so the migration from Galileo can be staged per-agent.
What's new:
- runtime/otel_setup.py: TracerProvider with ParentBased(ALWAYS_ON)
sampler, BatchSpanProcessor, OTLPSpanExporter, and resource
attributes (service.name=adk-agent, do.trace.visibility=customer,
workspace/deployment fallback for dev).
- runtime/otlp_tracker.py: OTLPTracesTracker mirroring the legacy
tracker surface and mapping NodeExecution metadata to GenAI
semconv (gen_ai.operation.name, gen_ai.system, gen_ai.request.*,
gen_ai.usage.*, gen_ai.server.time_to_first_token,
gen_ai.tool.{name,call.id}, gen_ai.retrieval.query). Root span
carries gen_ai.conversation.id (from session-id header) and
gen_ai.evaluation.run_uuid (from evaluation-id header) for
AAPP-1163/AAPP-1164/AAPP-1165 trace-stream and conversation-log UX.
- runtime/multi_tracker.py: fan-out wrapper that prefers the legacy
trace_id for X-Gradient-Trace-Id during dual-write and falls back
to the OTel root trace_id once the legacy tracker is removed
(Phase 4 of AAPP-1170).
- decorator.py: extract W3C traceparent from request headers via
TraceContextTextMapPropagator and pass it as the parent context
to on_request_start; forward evaluation-id as evaluation_run_uuid.
- network_interceptor.py: register a RequestHook that injects the
current OTel context as a traceparent header on outbound httpx
calls to inference / KBaaS / external LLM provider URLs, so
Serverless and Dedicated Inference can join the same trace per
AAPP-1172. Extended INFERENCE_URL_PATTERNS to cover OpenAI,
Anthropic, Google, Cohere, x.ai for accurate gen_ai.system.
- helpers.py: _ensure_tracker dynamically constructs a single
tracker, two trackers wrapped in MultiTracker, or no tracker at
all based on the env-var flippers.
Conversation log redaction (parity with galileo_logger.py):
DIGITALOCEAN_CONVERSATION_LOGS_ENABLED is snapshotted on the
per-request state at on_request_start; when false we omit
gen_ai.input.messages / gen_ai.output.messages events and set
gen_ai.input.redacted / gen_ai.output.redacted markers, while
preserving token counts, model, durations, and error info.
Stringified payload events are byte-capped at 256 KB.
Tenant attribution (team_id / org_id) is sourced from env vars
only at SDK level; the AAPP-1160 collector resource processor is
the canonical source of truth in production. The SDK does not
perform any blocking DigitalOcean API lookups during tracker
construction.
Tests cover: GenAI semconv mapping for LLM/tool/retriever, root
span per-request attributes, redaction with payload omission,
256 KB payload cap, MultiTracker fan-out and trace_id fallback,
ParentBased sampling honoring upstream sampling decisions,
outbound traceparent injection, ingress traceparent extraction,
no DO API call at tracker init, and original exception type
preservation through record_exception.
Ref: AAPP-1159 (epic AAPP-1071)
jshah-do
approved these changes
May 8, 2026
Suppress optional LangGraph import warnings so CLI JSON mode stays machine-readable, and update the A2A integration to work with the current a2a-sdk APIs used in CI. This keeps the OTLP tracing PR green without changing its user-facing tracing behavior.
bbatha
approved these changes
May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wire OpenTelemetry GenAI-conventioned spans out of
gradient-adkto a local OTLP collector while preserving the existing decorator API (@entrypoint,@trace_llm/@trace_tool/@trace_retriever,add_*_span) and theX-Gradient-Trace-Idheader. Traces are emitted via OTLP/HTTP-proto toOTEL_EXPORTER_OTLP_ENDPOINT(defaulthttp://localhost:4318), with aMultiTrackerfan-out so we can dual-write to the legacy DO Traces API behind two env-var flippers during the Galileo decommissioning window.This is the library-side migration for the AAPP-1071 Tracing & Observability epic. The collector deployment for ADK pods is AAPP-1160, and customer pods that don't pull a fresh ADK release migrate transparently via the AAPP-1171 Galileo proxy — so this lands behind
DIGITALOCEAN_OTLP_WRITE_ENABLED=falseby default and does not force any rollout on its own.What's in the box
runtime/otel_setup.py— one-timeTracerProviderwithParentBased(root=ALWAYS_ON)sampler,BatchSpanProcessor(5s timeout, queue 2048, drop on full), andOTLPSpanExporter. Resource attributes:service.name=adk-agent,service.version,do.trace.visibility=customer, plusagent_workspace_name/agent_deployment_nameas a dev/test fallback (the AAPP-1160 collector resource processor is the canonical source in prod).runtime/otlp_tracker.py—OTLPTracesTrackermirroring the legacy tracker surface. Maps existingNodeExecution.metadataflags to GenAI semconv:is_llm_call→chatspan withgen_ai.system,gen_ai.request.model,gen_ai.request.temperature,gen_ai.usage.{input,output,total}_tokens,gen_ai.server.time_to_first_tokenis_tool_call→execute_toolspan withgen_ai.tool.name,gen_ai.tool.call.idis_retriever_call→retrievalspan withgen_ai.retrieval.queryis_workflow/is_agent_call→invoke_agentspan (sub-spans become real OTel children via active context)Status.ERROR+record_exception(original_exception)with traceback preservedgen_ai.input.messages,gen_ai.output.messages), redaction-aware and byte-capped at 256 KBruntime/multi_tracker.py— fan-out wrapper. Returns the legacy DO trace_id forX-Gradient-Trace-Idduring dual-write and falls back to the OTel root trace_id once the legacy tracker is removed (Phase 4 of AAPP-1170).decorator.py— extracts W3Ctraceparentfrom request headers viaTraceContextTextMapPropagatorand passes it as the parent context toon_request_start; forwardsevaluation-idasevaluation_run_uuid(cross-ticket requirement from AAPP-1165).network_interceptor.py— registers aRequestHookthat injects the current OTel context astraceparenton outbound httpx calls to inference / KBaaS / external LLM provider URLs, so Serverless and Dedicated Inference can join the same trace per AAPP-1172.INFERENCE_URL_PATTERNSis extended to coverapi.openai.com,api.anthropic.com,generativelanguage.googleapis.com,api.cohere.ai,api.x.aifor accurategen_ai.systemderivation.runtime/helpers.py—_ensure_tracker()constructsDigitalOceanTracesTrackerand/orOTLPTracesTrackerbased onDIGITALOCEAN_GALILEO_WRITE_ENABLED(default true) andDIGITALOCEAN_OTLP_WRITE_ENABLED(default false), wrapping inMultiTrackerwhen both are enabled. OTLP-only mode no longer requires a DO API token.Conversation log redaction (Galileo parity)
DIGITALOCEAN_CONVERSATION_LOGS_ENABLEDis snapshotted onto the per-request state aton_request_start(so a mid-request env flip cannot partially redact a single trace). Whenfalse,gen_ai.input.messagesandgen_ai.output.messagesevents are omitted andgen_ai.input.redacted=true/gen_ai.output.redacted=truemarkers are set. Token counts, model, temperature, durations, tool names, and error status are always preserved — parity withcheck_and_remove_sensitive_data()ingenai-agent/logger/galileo_logger.py.Env-var contract (per-agent, materialized by cluster-api per AAPP-1167)
DIGITALOCEAN_GALILEO_WRITE_ENABLEDtrueDIGITALOCEAN_OTLP_WRITE_ENABLEDfalseDIGITALOCEAN_CONVERSATION_LOGS_ENABLEDtrueAgent.ConversationLogsEnabled)OTEL_EXPORTER_OTLP_ENDPOINThttp://localhost:4318OTEL_EXPORTER_OTLP_TRACES_ENDPOINTDIGITALOCEAN_TEAM_ID/DIGITALOCEAN_ORG_IDDISABLE_TRACESTest plan
tests/runtime/test_otlp_tracker.py— GenAI semconv attributes for chat / execute_tool / retrieval spans; root-spangen_ai.conversation.idandgen_ai.evaluation.run_uuid; resource attributes (service.name=adk-agent,do.trace.visibility=customer,team_id,org_id); error path setsStatus.ERROR.tests/runtime/test_otlp_tracker_lifecycle.py—__init__does not callclient.list_agent_workspaces;record_exceptionreceives the originalBaseException(class + message preserved through to theexceptionevent); redaction flag is snapshotted aton_request_start(mid-request env flip is ignored).tests/runtime/test_redaction.py—DIGITALOCEAN_CONVERSATION_LOGS_ENABLED=falseomitsgen_ai.input.messages/gen_ai.output.messagesevents and setsgen_ai.{input,output}.redacted=true; token counts and model still present.tests/runtime/test_payload_cap.py— 1 MB synthetic payload truncates to ≤ 256 KB with...<truncated>marker.tests/runtime/test_multi_tracker.py— fan-out hits both backends;submit_and_get_trace_idprefers legacy trace_id and falls back to OTel trace_id when legacy is None.tests/runtime/test_sampling.py—ParentBased(ALWAYS_ON)honors remote unsampled parent (no spans recorded; outboundtraceparentcarriesflags=00); records when remote parent is sampled (outboundflags=01).tests/decorator_test.py— extended fortraceparentextraction into a parent context andevaluation-id→evaluation_run_uuid. All existing decorator tests still pass.tests/runtime/helpers_test.py— extended for OTLP-only mode without DO API token.pydantic_ai/crewaideps), 0 failed (tests/test_a2askipped due to unrelateda2a-sdkpackaging mismatch onmain).OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 DIGITALOCEAN_OTLP_WRITE_ENABLED=true python main.pyagainstotelcol-contrib --config debug-exporter— to be done by reviewer / before enable-flag flip.Out of scope
time_to_first_token_nsis only populated for non-streamed responses today — separate follow-up against AAPP-1175).Ref: AAPP-1159 (epic AAPP-1071)