OpenTelemetry experimental
mise can export traces (and, separately, task stdout/stderr logs) for mise run to any OpenTelemetry-compatible backend such as Jaeger, Grafana Tempo, or SigNoz.
This is useful when you want to answer questions like:
- Which task is slow?
- Which task failed?
- What did a task print to stdout/stderr? (requires log export, see below)
- Which part of a monorepo run did a task belong to?
Quick Start
Enable OpenTelemetry trace export and set your collector endpoint:
[settings]
otel.enabled = true
# Optionally also ship task stdout/stderr as OTLP logs.
# Read the privacy notes below before turning this on.
otel.logs = trueexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318Then run your tasks as usual:
mise run build ::: test ::: lintIf your collector is reachable, mise will export:
- spans for individual tasks
- grouped spans for monorepo task roots
- a root span covering the whole
mise run, with child spans for setup such as tool installs - task logs from stdout/stderr — only when
otel.logsis also enabled
Configuration
mise uses the standard OpenTelemetry environment variables for configuration. The mise-specific settings are opt-in gates — they prevent mise from unexpectedly emitting telemetry in environments that set OTEL_EXPORTER_OTLP_* for other tools, and they keep log export (which is a larger privacy/security boundary) separate from trace export.
| Setting | Env Var | Default | Description |
|---|---|---|---|
otel.enabled | MISE_OTEL_ENABLED | false | Enable OpenTelemetry trace export for task executions. |
otel.logs | MISE_OTEL_LOGS | false | Enable OpenTelemetry log export for task stdout/stderr (see Privacy). |
Traces and logs are gated independently:
- Traces are exported only when
otel.enabled = trueand a traces endpoint is configured (OTEL_EXPORTER_OTLP_ENDPOINTorOTEL_EXPORTER_OTLP_TRACES_ENDPOINT). - Logs are exported only when
otel.logs = trueand a logs endpoint is configured (OTEL_EXPORTER_OTLP_ENDPOINTorOTEL_EXPORTER_OTLP_LOGS_ENDPOINT).
Setting otel.enabled does not, by itself, ship any task output to the collector.
Standard OTEL Environment Variables
When trace and/or log export is enabled, mise reads the following standard env vars:
| Env Var | Description |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | General OTLP endpoint (e.g. http://localhost:4318). |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | Signal-specific traces endpoint. Takes priority over the general endpoint. |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | Signal-specific logs endpoint. Takes priority over the general endpoint. |
OTEL_EXPORTER_OTLP_HEADERS | Headers for export requests (comma-separated key=value pairs), e.g. for auth. |
OTEL_EXPORTER_OTLP_TRACES_HEADERS | Signal-specific traces headers. Takes priority over the general headers. |
OTEL_EXPORTER_OTLP_LOGS_HEADERS | Signal-specific logs headers. Takes priority over the general headers. |
OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf (default) or http/json. gRPC is not supported. |
OTEL_SERVICE_NAME | The service.name resource attribute (defaults to mise). |
OTEL_RESOURCE_ATTRIBUTES | Additional resource attributes (comma-separated key=value pairs). |
Example with authentication:
export MISE_OTEL_ENABLED=1
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com:4318
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer mytoken"Example with resource attributes:
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=staging,team.name=platform"What You See
Each mise run creates one trace.
That trace contains:
- a root span covering the whole
mise run - setup spans for fetching remote tasks, resolving tasks, installing tools, running deps providers, and starting daemons
- task spans for individual tasks
- monorepo group spans when tasks come from different
config_roots
Typical shape:
mise run ← root span
├── resolve tasks ← setup span
├── install tools ← setup span
├── deps ← setup span
├── start daemons ← setup span
├── packages/frontend ← monorepo group span
│ ├── lint ← task span
│ ├── typecheck ← task span
│ └── build ← task span
├── packages/backend ← monorepo group span
│ └── test ← task span
└── deploy ← task span (direct child of root)For monorepos, this makes it easier to see which package or subproject a task came from. See Monorepo Tasks for background on config_root.
Span Timing
Spans are live for exactly as long as the thing they measure, so durations nest the way you'd expect: the root span opens once the tasks to run are known and closes after the last task finishes. A group span opens with its first task and stays open until the run ends, since mise can't know whether another task from that package is still to come.
Setup runs inside the root span, each phase under its own span with mise.span_type = "setup": fetching remote tasks (only when there are any), resolving the task graph, installing missing tools, running automatic deps providers, and starting daemons. A slow first run on a fresh CI runner shows up as a long install tools span, not as unexplained time before the first task. A failed phase is marked as an error, and so is the root span.
The rest of the root span is scheduler overhead, per-task queueing on the jobs semaphore, and per-task toolset and environment resolution. A task span starts when the scheduler picks the task up, not when its process is spawned, so the gap between a task span's start and its first output is mise's own per-task setup rather than the task itself.
Task spans include attributes such as:
| Attribute | Description |
|---|---|
mise.task.name | Task name |
mise.task.args | CLI arguments passed to the task (space-joined) |
mise.task.source | Path to the config file defining the task |
mise.task.config_root | Config root directory (for monorepo tasks) |
mise.task.skipped | true when the task was skipped because sources were up to date |
mise.task.cancelled | true when the task was stopped because a sibling task failed |
process.command_args | Full argv as a string array (["mise", task_name, ...args]), per OTel CLI semantic conventions |
process.exit.code | Exit code of the task as an integer (0 for success/skipped, propagated from the failed command otherwise) |
Failed vs. Cancelled Tasks
By default a failing task stops its siblings (unless --continue-on-error is set), so several tasks can end at once from a single fault. Only the task that actually failed gets an Error span status; the siblings mise shut down are recorded with an Unset status and mise.task.cancelled = true, so a search for errored spans returns the one real cause rather than every task that happened to be running. Because they were terminated by a signal and have no exit code of their own, cancelled tasks carry no process.exit.code. The root span is still marked Error, since the run as a whole failed.
A sibling that exits with its own non-zero status after the first failure is still an Error, and so is one that crashes: only tasks ended by the SIGTERM mise sends count as cancelled. Windows reports a terminated process as an ordinary exit code, so there every task that ends after the first failure is recorded as cancelled.
A task whose tools fail to install never starts, but it still gets an Error span, since its failure is what stopped the run.
Error messages in span status go through the same redactions as task args.
When --timeout expires, the root span is ended as an Error and flushed. Tasks still running at that point are not exported.
Logs
Log export is a separate, explicit opt-in (otel.logs = true / MISE_OTEL_LOGS=1) because shipping task stdout/stderr to the collector is a different trust boundary from trace export. Read Privacy and Trust Boundary before enabling it.
When enabled, each line of task stdout and stderr is exported as an OTLP log record linked to the corresponding task span, so you can inspect output directly from the trace. The link needs the spans too: with otel.logs but not otel.enabled, records still carry trace and span IDs, but no spans are exported for them to point at.
- stdout is exported with severity
INFO - stderr is exported with severity
WARN(many tools write progress, diagnostics, and compiler warnings to stderr that are not errors — actual failure is conveyed by the task span status and theprocess.exit.codeattribute)
Nested mise run
When a task shells out to mise run, the inner run's output flows up through the outer task's pipe, so both processes see the same lines. mise hands each task a claim directory (MISE_TASK_OTEL_LOG_CLAIM); a nested run that exports its own task logs registers there while it is alive, and the outer run skips exporting for as long as any nested run is registered. Each line is therefore exported exactly once, by the innermost run that knows which task actually produced it:
[tasks.outer]
run = "echo building; mise run inner; echo done"building and done are attributed to the outer task span, and everything inner prints is attributed to the inner task span. Terminal output is unaffected — the claim only gates log export. Traces are unaffected too: a nested run still contributes its spans to the same trace via TRACEPARENT.
A nested run only takes over if it exports logs itself, so disabling otel.logs for the inner run leaves the outer run reporting its output as before. Each registration records the owning process, so one that dies without releasing it — killed with SIGKILL, for instance — is detected as stale and the outer run resumes exporting. Nested runs started concurrently from a single task (mise run a & mise run b &) each register, so the outer run stays quiet until the last of them exits.
A nested mise run --raw never reads its tasks' output, so it doesn't claim the stream and the outer run keeps exporting it. Within a nested run that does claim the stream, a task that is itself raw (or interactive) is exported by neither run: the nested run doesn't read its output and the outer run defers to the nested one. Terminal output is unaffected.
TIP
Every output mode that reads task output line by line exports it: prefix, keep-order, timed, replacing, interleave, and quiet. The silent output mode and output a task silences are not read, so they are not exported. With --raw, output goes straight to the terminal and is not exported either.
In interleave/quiet mode mise normally hands the task the terminal directly. While log export is on it keeps a pipe instead so it can read every line, so the task no longer sees a TTY. This can change buffering, colour output, progress bars, prompts, and any isatty()-dependent behaviour. Run affected tasks under --raw to keep a real TTY (at the cost of log export for that task).
Privacy and Trust Boundary
Exporting traces and logs ships information about your tasks to your OpenTelemetry collector. Even though all of this is visible locally already, the collector is a different trust boundary — anything sent there may be stored, indexed, queryable by other users of that backend, and retained according to its policy.
What trace export (otel.enabled) sends per task:
- the task name, display name, args, config source, and config root
process.command_args(the full argv as a string array, per OTel CLI semconv)process.exit.code- timing and span status
What log export (otel.logs) additionally sends:
- every line written to the task's stdout
- every line written to the task's stderr
Implications:
- Secrets in args. If a secret appears in
mise.task.args/process.command_args(for examplemise run deploy -- --token=hunter2), trace export will ship it to the collector unless it matches one of your redactions. Prefer passing secrets via environment variables, which are never exported. - Secrets in output. With
otel.logs = true, any secret that a task writes to stdout/stderr is shipped to the collector. This includes anything the task receives in env vars and accidentally echoes (e.g. viaset -x, debug logging, or shell tracing). - Redaction. mise's terminal redaction (
redactions = […]inmise.toml) applies before lines are forwarded to the OTLP log pipeline, so redacted values are also redacted in exported logs, and so are task args on log records. However, redaction only covers values you've explicitly listed — it does not detect arbitrary secrets in output. --raw.--rawbypasses mise's line capture entirely, so task output goes straight to the terminal and is not exported as logs. Note that this also disables redactions for that task.
If you don't want task output leaving the machine, leave otel.logs = false (the default) and rely on trace export alone.
Example: Local Development with Jaeger
Start Jaeger with OTLP/HTTP support:
docker run -d --name jaeger \
-p 16686:16686 \
-p 4318:4318 \
jaegertracing/all-in-one:latestConfigure mise:
[settings]
otel.enabled = trueexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318Now run any mise task and open http://localhost:16686.
Trace Propagation
mise propagates trace context to child processes using the OpenTelemetry Environment Carriers spec via the TRACEPARENT and TRACESTATE env vars (W3C Trace Context format). This means:
- Nested
mise runinvocations automatically join the parent trace. - Any OTEL-instrumented tool a task invokes (Node.js, Go, Python, etc.) will automatically parent its spans under the mise task span — no mise-specific integration needed.
Notes
- When neither
otel.enablednorotel.logsis set, mise does not create trace context or export any telemetry. - Export failures are logged at debug level and never break task execution.
- Each export request times out after 3 seconds unless
OTEL_EXPORTER_OTLP_TIMEOUT(orOTEL_EXPORTER_OTLP_TRACES_TIMEOUT/OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) says otherwise, so an unreachable collector delays the end ofmise runby at most that much. - Offline mode (
--offline/MISE_OFFLINE=1) turns export off. - Task args in span names and attributes go through the same redactions as terminal output.
service.namedefaults tomiseunlessOTEL_SERVICE_NAMEorservice.nameinOTEL_RESOURCE_ATTRIBUTESsets it.