Version v0.4.0 of the documentation is no longer actively maintained. The site that you are currently viewing is an archived snapshot. For up-to-date documentation, see the latest version.

ADR 0010: Observability Stack from Day One

ADR 0010: Observability Stack from Day One

Status: Accepted Date: 2026-05-21

Context

One of Leoflow’s stated differentiators is native observability, which Airflow famously lacks. This must be backed by actual implementation from the first commit, not bolted on later.

Decision

Every Leoflow binary ships with three pillars of observability built in:

  1. Metrics — Prometheus, exposed at /metrics.
  2. Tracing — OpenTelemetry, exporting via OTLP to any compatible collector (Jaeger, Tempo, Honeycomb, Datadog).
  3. Logs — Structured JSON via log/slog, with consistent fields (trace_id, span_id, dag_id, task_id, run_id).

All three are wired together via OpenTelemetry context propagation, so a log line, a trace span, and a metric label can be correlated.

Required Metrics for the MVP

The following metrics must exist before the MVP is considered complete. They are the contract Leoflow makes with operators.

Scheduler

MetricTypeLabels
leoflow_scheduler_loop_duration_secondsHistogram
leoflow_scheduler_decisions_totalCounterdecision_type (schedule/skip/defer)
leoflow_scheduler_leaderGaugereplica_id
leoflow_active_dag_runsGaugedag_id, state
leoflow_queued_tasksGaugedag_id

Task Lifecycle

MetricTypeLabels
leoflow_task_state_transitions_totalCounterfrom_state, to_state, dag_id
leoflow_task_duration_secondsHistogramdag_id, task_id, task_type
leoflow_task_retries_totalCounterdag_id, task_id
leoflow_task_pod_creation_duration_secondsHistogram
leoflow_task_cold_start_secondsHistogramdag_id

XCom

MetricTypeLabels
leoflow_xcom_size_bytesHistogramdag_id
leoflow_xcom_push_totalCounterdag_id
leoflow_xcom_pull_totalCounterdag_id
leoflow_xcom_rejected_totalCounterreason (too_large/schema_mismatch/expired)

API

MetricTypeLabels
leoflow_http_requests_totalCountermethod, path, status
leoflow_http_request_duration_secondsHistogrammethod, path
leoflow_auth_failures_totalCounterreason

Executor (K8s)

MetricTypeLabels
leoflow_pods_created_totalCounterdag_id, result (success/error)
leoflow_pods_runningGauge
leoflow_pod_pending_duration_secondsHistogram
leoflow_kubernetes_api_calls_totalCounteroperation, result

Tracing

Every task instance gets a root span with these attributes:

  • leoflow.dag_id
  • leoflow.task_id
  • leoflow.run_id
  • leoflow.try_number

Child spans:

  • scheduler.decision
  • executor.create_pod
  • agent.fetch_xcom
  • agent.execute_user_code
  • agent.push_xcom

The trace continues across the gRPC boundary between the Control Plane and the Agent via standard OTel context propagation.

Logs

Every log line is JSON, written to stdout. Common fields:

{
  "time": "2026-05-21T14:23:11.482Z",
  "level": "INFO",
  "msg": "task transitioned to RUNNING",
  "trace_id": "abc123...",
  "span_id": "def456...",
  "dag_id": "etl_vendas",
  "task_id": "extract",
  "run_id": "scheduled__2026-05-21",
  "try_number": 1,
  "tenant_id": "default"
}

No human-readable formatters in production builds. JSON only. Operators use jq or log aggregators.

Health Checks

Every binary exposes:

  • /healthz — liveness. Returns 200 if the process is alive.
  • /readyz — readiness. Returns 200 only if dependencies (Postgres, Redis) are reachable.

K8s deployments use these for liveness and readiness probes.

Consequences

  • The dependency footprint grows. client_golang, go.opentelemetry.io/otel, and slog are mandatory.
  • Performance overhead is real but small. Metrics and traces are cheap when batched. Logs at INFO level are negligible.
  • The CI pipeline must validate that every new metric is registered with a description and that no metric explodes label cardinality.
  • Operators get a Grafana dashboard template shipped with the project (in helm/dashboards/).

Alternatives Rejected

  • Add observability later: rejected because retrofitting tracing across an existing codebase is enormously expensive.
  • Only logs, no metrics or traces: rejected because logs alone cannot answer “is the system slow right now?”.
  • Custom metrics format: rejected because Prometheus is the industry standard.