LanternDocs

Observability

One OTel trace per spawn, GenAI token telemetry, real-time anomaly detection — wired through standard OpenTelemetry.

In plain termsObservability answers "what is my agent doing right now, and what did it do last Tuesday?" Every run produces a trace — a timeline of each step with its timing, model, tokens, and cost — and every trace is tagged with who, which run, and which step, so you can filter straight to the one that matters. It plugs into the monitoring tools teams already use (Grafana, Datadog, anything OpenTelemetry- compatible), and it's free when switched off.
One run, one story Every signal carries the same identifiers: lantern.tenant_id, run_id, step_id and user_id. Six signals: OTel traces, lantern.run.* metrics, the journal_events timeline streamed over SSE and gRPC, peer-service health from TCP probes, externally verifiable Ed25519 receipts over a SHA-256 of the journal, and cost attribution from usage rollups. One run, one story Every signal speaks the same identifiers, so nothing needs correlation glue. The attribute contract, stamped everywhere tenant_id · run_id · step_id · user_id on every span (internal/middleware.EnrichSpan) Traces: OTel spans with the attribute contract Traces (OTel) HTTP entry span, named by route template Step spans, then LLM spans (gen_ai.*) No-op safe when OTel is not enabled Metrics: lantern.run.* histograms and counters Metrics (OTel) lantern.run.step.duration (histogram) lantern.run.step.retries · .total lantern.run.replay.skips · .budget.blocks replay.skips is crash-resume health journal_events: the event-sourced run timeline Journal step_started, step_completed, step_failed, confidence_evaluated: the run waterfall SSE with 15 s heartbeat, 500 ms tail, and gRPC GET /v1/runs/{id}/events Peer health sweep: GET /v1/system/health Peer health Periodic TCP probes of peer services Down after 3 straight failures One owner alert per transition GET /v1/system/health Verifiable receipts: signed proof of execution (Ed25519 when a seed is configured) Receipts SHA-256 over the full journal stream Ed25519 signature; verify with no account POST /v1/runs/receipts/verify /.well-known/lantern-receipts Cost attribution: usage rollups, budgets, forecasts Cost attribution Per-call usage into agent_usage_daily Feeds budget enforcement (402) Forecasts and /evaluations analytics tokens and costUsd on every completion What this buys you at 3 AM A failing run: the exact step, its trace, what it cost. One tenant_id and run_id filters all of it.
Every signal — OTel span, metric, journal row, health probe, receipt — speaks the same identifiers, so nothing needs correlation glue.

One trace per spawn

Every run opens a single OTel trace. Spans from every entry point use the same attribute keys, defined in internal/middleware/span.go and stamped by a single EnrichSpan helper so they never drift between HTTP and gRPC code paths:

lantern.tenant_id   # on every control-plane HTTP/gRPC span
lantern.user_id
lantern.run_id
lantern.step_id     # per-step spans in the inline executor

On the microVM tier, scheduler spans carry tenant_id and vm_id; the per-VM isolation class is recorded as a container/pod label, and the trace context propagates through the manager into the harness (see Identity & secrets for the per-spawn identity). A durable resume starts a new trace tagged with the same lantern.run_id.

Trace spine
gateway.requesttenant_id
control-plane: run dispatchrun_id
model-router: routestep_id · model_used · tokens · cost_usd
runtime-scheduler: scheduletenant_id · vm_id
runtime-manager → harnesstraceparent propagated
W3C traceparent propagated at every boundary · a durable resume starts a new trace with the same lantern.run_id

Enabling OTel

Export is env-gated. Set the endpoint and traces flow; leave it unset and tracing is a no-op (zero overhead):

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
# or
LANTERN_OTEL_ENABLED=1   # uses default localhost endpoint
W3C traceparent is always active. Inbound trace context is forwarded correctly even without an exporter configured.

GenAI semantic conventions

LLM steps are annotated with OTel GenAI semantic-convention attributes — including reasoning tokens and cache tokens, not just plain input/output counts. Per-step cost attribution and model-usage breakdowns work out of the box with any OTel-compatible backend.

OTel span attribute contract

Control-plane spans use the first four keys, set via internal/middleware.EnrichSpan. They are no-ops when telemetry is disabled.

AttributeKeySet by
Tenantlantern.tenant_idHTTP enrichment middleware + gRPC tracing interceptor
Userlantern.user_idSame
Runlantern.run_idSame + inline executor
Steplantern.step_idInline executor per step
VM ID (microVM)vm_idScheduler spans
Model usedmodel_usedModel-router completion span
Cost USDcost_usdModel-router completion span
Tokens in / outtokens_in / tokens_outModel-router completion span

The five lantern.run.* metrics

The inline executor emits five OTel metric instruments via the lantern.runtime meter (internal/middleware/metrics.go). They are no-ops when the global MeterProvider is unset (the default no-op provider is safe to import with zero overhead).

InstrumentTypeAttributesDescription
lantern.run.step.durationHistogram (ms)lantern.agent_name, lantern.node_type, lantern.tier, lantern.outcomeWall-clock duration of one workflow step, including all retry backoff. outcome is ok, failed, or retried.
lantern.run.step.retriesCounterlantern.agent_name, lantern.node_type, lantern.tierExtra attempts beyond the first. Only incremented when retryCount > 0.
lantern.run.replay.skipsCounterlantern.agent_nameCompletedStep cache hits during crash-resume. Each hit means one node was skipped rather than re-executed.
lantern.run.budget.blocksCounterlantern.agent_nameRuns denied by the agent's hard-fail budget policy (HTTP 402).
lantern.run.totalCounterlantern.agent_name, lantern.tier, lantern.statusTotal runs dispatched through the inline executor. lantern.tier is currently always shared; lantern.status is succeeded or failed (workflow runs whose steps failed are counted as succeeded, a known gap).

Real-time anomaly detection

The runtime watches the live event stream for pathological shapes — a tool-call loop, a step retrying without progress — and surfaces them in real time. This is the early-warning layer for runaway runs.

Peer-service health sweep

A background loop in the control-plane TCP-probes its peer services every 60 s (LANTERN_HEALTH_SWEEP_INTERVAL; set "0" or "off" to disable). After 3 consecutive failures it declares a peer DOWN and sends one self-chat alert. It sends one more when the peer recovers. No alert storms — only state transitions fire notifications.

Services probed when their address env var is set:

  • model-router — LANTERN_MODEL_ROUTER_ADDR (only when LANTERN_USE_MODEL_ROUTER=1 or addr is non-default)
  • runtime-scheduler — LANTERN_SCHEDULER_GRPC_ADDR
  • runtime-manager — LANTERN_DEFAULT_MANAGER_ADDR
  • workflow-engine — LANTERN_WORKFLOW_ENGINE_ADDR (optional)

Read the current snapshot:

GET /v1/system/health    # JWT-authed
{
  "services": [
    {
      "name": "runtime-manager",
      "addr": "localhost:50054",
      "up": false,
      "consecutiveFailures": 5,
      "lastChecked": "2026-07-23T10:00:00Z",
      "lastTransition": "2026-07-23T09:57:00Z"
    }
  ]
}

Metrics endpoint

Per-VM live stats for the caller's tenant:

GET /v1/runtime/metrics

Returns a vmMetricsDTO array with vmId, state, node, az, isolationClass, promMetrics (raw Prometheus text from the harness), and timestamps. Per-instance detail: GET /v1/runtime/vms/{id}. Live log stream: GET /v1/runtime/vms/{id}/logs (SSE). The dashboard runtime page renders all three.

Gateway and model-router traces

The gateway emits one span per HTTP request (gateway.request, tagged with tenant_id) via OTLP/HTTP. The model-router emits one span per routing call tagged with tenant_id, run_id, step_id, model_used, tokens_in/out, cost_usd, and escalated via OTLP/gRPC. Both honour inbound W3C traceparent, so spans join the caller's distributed trace automatically.

No Prometheus histograms yet for gateway / model-router. Latency SLOs live in your tracing backend (Tempo / Jaeger / Honeycomb). Alert rules that would cover p99 latency are parked in the lantern-TODO-needs-instrumentation group in infra/monitoring/prometheus/alerts.yml until the histogram metric ships.

Prometheus alerts, dashboards, runbooks

Production monitoring artifacts live in infra/monitoring/:

GroupAlertsSource
lantern-schedulerSchedulerDown · SchedulerNoLeader · SchedulerScheduleErrorRateHigh · SchedulerQuotaRejectionSurge · SchedulerNoRegisteredNodesruntime-scheduler :8085/metrics
lantern-livenessControlPlaneDown · ControlPlaneNotReady · GatewayDown · ModelRouterDownup scrape + blackbox /readyz
lantern-postgresPostgresExporterDown · PostgresConnectionSaturation · DataPlaneHeartbeatStale · CronScheduleOverduepostgres_exporter + custom queries

Operator runbooks live in docs/runbooks/ and are linked from each alert's runbook: annotation in alerts.yml. Grafana dashboards: grafana/platform-overview.json and grafana/data-plane-runtime.json.