Skip to content

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:

mise.toml
toml
[settings]
otel.enabled = true
# Optionally also ship task stdout/stderr as OTLP logs.
# Read the privacy notes below before turning this on.
otel.logs = true
bash
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

Then run your tasks as usual:

bash
mise run build ::: test ::: lint

If 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.logs is 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.

SettingEnv VarDefaultDescription
otel.enabledMISE_OTEL_ENABLEDfalseEnable OpenTelemetry trace export for task executions.
otel.logsMISE_OTEL_LOGSfalseEnable OpenTelemetry log export for task stdout/stderr (see Privacy).

Traces and logs are gated independently:

  • Traces are exported only when otel.enabled = true and a traces endpoint is configured (OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).
  • Logs are exported only when otel.logs = true and a logs endpoint is configured (OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_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 VarDescription
OTEL_EXPORTER_OTLP_ENDPOINTGeneral OTLP endpoint (e.g. http://localhost:4318).
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTSignal-specific traces endpoint. Takes priority over the general endpoint.
OTEL_EXPORTER_OTLP_LOGS_ENDPOINTSignal-specific logs endpoint. Takes priority over the general endpoint.
OTEL_EXPORTER_OTLP_HEADERSHeaders for export requests (comma-separated key=value pairs), e.g. for auth.
OTEL_EXPORTER_OTLP_TRACES_HEADERSSignal-specific traces headers. Takes priority over the general headers.
OTEL_EXPORTER_OTLP_LOGS_HEADERSSignal-specific logs headers. Takes priority over the general headers.
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobuf (default) or http/json. gRPC is not supported.
OTEL_SERVICE_NAMEThe service.name resource attribute (defaults to mise).
OTEL_RESOURCE_ATTRIBUTESAdditional resource attributes (comma-separated key=value pairs).

Example with authentication:

bash
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:

bash
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:

AttributeDescription
mise.task.nameTask name
mise.task.argsCLI arguments passed to the task (space-joined)
mise.task.sourcePath to the config file defining the task
mise.task.config_rootConfig root directory (for monorepo tasks)
mise.task.skippedtrue when the task was skipped because sources were up to date
mise.task.cancelledtrue when the task was stopped because a sibling task failed
process.command_argsFull argv as a string array (["mise", task_name, ...args]), per OTel CLI semantic conventions
process.exit.codeExit 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 the process.exit.code attribute)

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:

toml
[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 example mise 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. via set -x, debug logging, or shell tracing).
  • Redaction. mise's terminal redaction (redactions = […] in mise.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. --raw bypasses 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:

bash
docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4318:4318 \
  jaegertracing/all-in-one:latest

Configure mise:

mise.toml
toml
[settings]
otel.enabled = true
bash
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

Now 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 run invocations 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.enabled nor otel.logs is 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 (or OTEL_EXPORTER_OTLP_TRACES_TIMEOUT / OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) says otherwise, so an unreachable collector delays the end of mise run by 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.name defaults to mise unless OTEL_SERVICE_NAME or service.name in OTEL_RESOURCE_ATTRIBUTES sets it.
MIT LicenseCopyright © 2026jdx.dev