dagster-otel
OpenTelemetry tracing for Dagster ops and assets -- with trace/span IDs correlated into your own log lines -- without giving up ownership of your op/asset definitions to a third-party decorator, and without monkeypatching Dagster internals.
Status: early release, self-tested locally against real Dagster runs (multiprocess,
k8s_job_executor, retry-from-failure) + a real trace backend.
Table of Contents
Installation
pip install dagster-otel
Usage
from dagster import asset, job, op
from dagster_otel import traced
@op(...) # Dagster's own @op still owns op-ness; @traced() is a thin
@traced() # layer underneath. No @resource/required_resource_keys, no
def upstream_op(context) -> int: # manual "root" step -- the first @traced() step to
... # run in a given run just becomes the root.
@op(...)
@traced()
def downstream_op(context, x: int) -> int:
...
@asset(...)
@traced() # same decorator, works for assets too
def downstream_asset(context) -> None:
...
@op(...)
@traced # bare works too, like @op/@asset themselves -- same as @traced()
def another_op(context) -> None:
...
@job(...)
def my_job():
downstream_op(upstream_op())
Works across Dagster's multiprocess and k8s_job_executor executors: each step
usually runs in its own process, sometimes on its own node, so trace context is
propagated via Dagster's own run storage rather than in-process memory. See
docs/design.md for how, and what's verified vs. still assumed.
For @dbt_assets, dagster_otel.dbt.traced_dbt() is a drop-in replacement for
@traced() that additionally opens a child span per dbt node (model/seed/test),
keyed by the real Dagster asset_key/check_name -- no changes needed to the function
body:
from dagster_otel.dbt import traced_dbt
@dbt_assets(manifest=...)
@traced_dbt()
def my_dbt_assets(context, dbt: DbtCliResource):
yield from dbt.cli(["build"], context=context).stream()
Configuration
Standard OTel environment variables -- nothing bespoke:
| Variable | Purpose |
|---|---|
OTEL_SERVICE_NAME |
Names your service in the trace backend. |
OTEL_EXPORTER_OTLP_ENDPOINT (or ..._TRACES_ENDPOINT) |
Where to send spans (e.g. http://localhost:4317). Required -- without one of these set, no real exporter is attached at all (see below). |
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT / ..._TIMEOUT |
Per-export timeout. Set this yourself if the default (2s) doesn't fit -- see docs/design.md for why a default exists at all (an unreachable collector otherwise blocked every step for ~7s). |
OTEL_SDK_DISABLED |
Set to true to force no export regardless of the endpoint vars above. |
@traced() reads these itself (idempotently) the first time it runs in a process --
there's nothing else to wire up, no @resource/required_resource_keys needed. Call
configure() yourself only if you want configuration to happen eagerly (e.g. at
Definitions load time) rather than lazily on first use.
Without OTEL_EXPORTER_OTLP_ENDPOINT/..._TRACES_ENDPOINT set, no real OTLP exporter
is created at all -- spans are still created (propagation and log correlation keep
working), just never sent anywhere, so trying @traced() with zero setup never makes
a surprise network call. See docs/design.md for the one deliberate
tradeoff this makes.
Nesting a whole run's trace under an external caller's (a CI/CD pipeline, a
scheduler, another OTel-instrumented system) is a run tag, not an env var -- set
EXTERNAL_TRACE_CONTEXT_TAG_KEY (exported from dagster_otel) at launch time:
from dagster_otel import EXTERNAL_TRACE_CONTEXT_TAG_KEY
carrier: dict[str, str] = {}
TraceContextTextMapPropagator().inject(carrier) # from your own active span
my_job.execute_in_process(tags={EXTERNAL_TRACE_CONTEXT_TAG_KEY: json.dumps(carrier)})
Every root step in the run (the ones that would otherwise seed a fresh trace) checks for this tag first. See docs/design.md for the full verification.
Compatibility
Built and verified against Dagster 1.13.22 and opentelemetry-sdk 1.44.0
(pyproject.toml/uv.lock) -- this is the combination every behavior described here
has actually been checked against, including the multiprocess/k8s_job_executor/
retry-from-failure verification in docs/design.md. pyproject.toml
declares a much wider floor (dagster >= 1.5) since nothing here relies on
version-specific Dagster internals beyond what's documented as an accepted-risk
private-API dependency there -- but that wide range isn't individually spot-checked
the way it is for dagster-prometheus-exporter.
If you hit an incompatibility on another version, please
open an issue.
Requires Python 3.10+ (matches Dagster's own floor).
Why this exists
See docs/design.md for the full rationale, including a comparison
against prior art (formenergy-observability,
a monkeypatch-based prototype) and the design decisions (no monkeypatching, decorators
stack under Dagster's own @op/@asset rather than replacing it, log correlation via
a public logging.Filter on context.log).
Contributing
See CONTRIBUTING.md for how to set up your toolchain, run checks locally, and submit a pull request. Bug reports and feature requests go through GitHub issues; a security vulnerability goes to SECURITY.md instead.
License
MIT -- see LICENSE.
Metadata
Release files for dagster-otel 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dagster_otel-0.2.0.tar.gz | 249.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dagster_otel-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 275.9 kB
Release files / dagster_otel-0.2.0.tar.gz
| Download URL | dagster_otel-0.2.0.tar.gz |
|---|---|
| Size | 249.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f9b41b96571313d5369a657baafb5ad254011f265446064f9f18145c795ae9e7
|
|
BLAKE2b-256 checksum How to use checksums |
101bfff665c70cc0eeca85c69a2b01254cadb64c60ed62dba16aeb0f5b80b3e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logRelease files / dagster_otel-0.2.0-py3-none-any.whl
| Download URL | dagster_otel-0.2.0-py3-none-any.whl |
|---|---|
| Size | 26.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0b0ef32f8e6a60bdb4b8ec49ff34ca0ecdeb7af1eeffd8b4a16c7f7bddedddca
|
|
BLAKE2b-256 checksum How to use checksums |
4180407ca525ab111c4f7087e78a9433350cf714f9c5c6e34d3ff14f7b0a7092
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency log