feat(utils): native OpenTelemetry tracing via dspy.enable_otel_tracing()#10042
Draft
isaacbmiller wants to merge 1 commit into
Draft
feat(utils): native OpenTelemetry tracing via dspy.enable_otel_tracing()#10042isaacbmiller wants to merge 1 commit into
isaacbmiller wants to merge 1 commit into
Conversation
Add dspy.OtelCallback and dspy.enable_otel_tracing(), a first-party OpenTelemetry integration built on the BaseCallback system. LM and tool spans follow the OTel GenAI semantic conventions (gen_ai.*); module, adapter, and evaluate spans carry dspy.*-namespaced attributes. Content capture (prompts, completions, tool args, module IO) is on by default and can be disabled per callback or via OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. Only opentelemetry-api is required, imported lazily and shipped as the new `otel` extra, so `import dspy` is unaffected without it. ParallelExecutor now runs each worker inside contextvars.copy_context() so OTel context and ACTIVE_CALL_ID propagate into pool threads; spans created inside Evaluate/Parallel workers parent correctly instead of starting orphan traces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Motivation
DSPy has no first-party, vendor-neutral tracing story. The two existing paths are:
mlflow.dspy.autolog()): built cleanly on DSPy'sBaseCallbacksystem, but exports only to MLflow.openinference-instrumentation-dspy): vendor-neutral OTLP, but bypasses the callback system entirely and monkey-patches DSPy internals withwrapt(LM.__call__,Predict.forward,Module.__call__,Adapter,Tool,Retrieve, plus custom copyable wrappers sodeepcopydoesn't break). It reaches intosignature.output_fields,Prediction._store, and__qualname__dispatch, and has broken across DSPy releases (e.g. [Bug] 2.6.0 Instrumentation issue #7627).This PR adds native OpenTelemetry emission built on the public callback surface, so any OTLP backend (Jaeger, Grafana, Langfuse, Phoenix via its convention-translation processor, Datadog, ...) works without third-party patching of DSPy internals:
What this looks like
A
dspy.ReActagent with one tool, exported to a local Jaeger via OTLP:LM spans carry GenAI-semconv attributes, including full structured messages (content capture is on by default, with an opt-out):
Design
OtelCallbackmapsBaseCallbackevents to spans; parenting rides the OTel context, so DSPy spans nest correctly under surrounding application spans and vice versa.gen_ai.*) for LM spans (chat {model}, CLIENT kind, provider/model/temperature/max_tokens,gen_ai.input.messages/gen_ai.output.messages,gen_ai.usage.*) and tool spans (execute_tool {name},gen_ai.tool.*). Since the GenAI conventions are still in Development status,semconv_version=1is an explicit knob so attribute layouts can evolve without breaking dashboards.dspy.*attributes for spans with no GenAI equivalent: modules (dspy.module.type,dspy.module.signature, content-gateddspy.module.input/output), adapters (ChatAdapter.format/.parsespans), andEvaluate(dspy.evaluate.num_examples,dspy.evaluate.score).capture_content=False) or via the standardOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTenv var.opentelemetry-api(newdspy[otel]extra), imported lazily insideOtelCallback.__init__;import dspyis unaffected when it isn't installed. Without a configured SDK the API is a no-op.LMResponse(explicitLMRequestcalls ordspy.context(experimental=True)). The legacy list-output path has no race-free per-call usage source; that needs a callback-payload enrichment and is deliberately left to a follow-up.ParallelExecutor contextvars fix
ParallelExecutorpreviously copied onlythread_local_overridesinto worker threads, so contextvars — the OTel context and the callback system'sACTIVE_CALL_ID— did not propagate, and spans created insideEvaluate/Parallelworkers became orphan traces. Workers now run insidecontextvars.copy_context()captured at submit time. This also fixes span parenting for other callback-based integrations (e.g. MLflow) acrossEvaluate/Parallel.Tests
tests/utils/test_otel.py(10 tests): span nesting/parenting forChainOfThought, content gating (param + env var), typed-pathgen_ai.usage.*, tool success/error spans with ERROR status + exception events, async calls,Evaluatespans parenting across worker threads, idempotentenable_otel_tracing(), unsupportedsemconv_versionrejection.tests/callback,tests/utils/test_parallelizer.py,tests/evaluate,tests/streaming(72 passed, 30 skipped).openai/gpt-5-nanoReAct program exported to Jaeger via OTLP (screenshots above).Not in this PR (planned follow-ups)
🤖 Generated with Claude Code