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 speaks the same identifiers — nothing needs correlation glue THE ATTRIBUTE CONTRACT — STAMPED EVERYWHERE tenant_id · run_id · step_id · user_id — stamped on every span FIVE SIGNALS, ONE CONTRACT Traces (OTel) HTTP entry span (route template, e.g. POST /v1/runs) → step spans → LLM spans (gen_ai.* semconv) → microVM report-ingest + resume spans no-op safe when LANTERN_OTEL_ENABLED unset Metrics (OTel, meter lantern.runtime) lantern.run.step.duration ms histogram · tier + outcome lantern.run.step.retries · lantern.run.replay.skips lantern.run.budget.blocks · lantern.run.total replay.skips is your crash-resume health signal Journal (the product-facing timeline) step_started · completed · failed · retrying · waiting · confidence_evaluated — rendered as the run waterfall, streamed live: SSE (15 s heartbeat, 500 ms tail) + gRPC GET /v1/runs/{id}/events Peer health (the sweep) TCP probes every 60 s · DOWN after 3 straight failures · one owner self-chat alert per transition — no storms GET /v1/system/health Receipts (externally verifiable) SHA-256 chain over the FULL journal (seq|kind|payload) → Ed25519 signature · verify with no Lantern account POST /v1/runs/receipts/verify · /.well-known/lantern-receipts Cost attribution per-call usage → agent_usage_daily rollups → budget enforcement (402) + /evaluations analytics + forecasts tokensIn / tokensOut / costUsd on every completion WHAT THIS BUYS YOU AT 3 AM a failing run: the exact step → its trace → what it cost — in one click-path One tenant_id + run_id filters all of it. No cross-referencing five dashboards. Verifiable receipts — Ed25519-signed proof of execution Traces — OTel spans with the attribute contract Metrics — lantern.run.* histograms and counters journal_events — the event-sourced run timeline Peer health sweep — GET /v1/system/health Cost attribution — usage rollups, budgets, forecasts
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 span, both tiers
lantern.user_id
lantern.run_id
lantern.step_id     # per-step spans in the inline executor

On the microVM tier the manager and harness additionally stamp vm_id, isolation_class, and agent_instance_id (the per-spawn Ed25519 identity — see Identity & secrets). A durable resume after a crash re-joins the same trace_id — the full lifecycle is one coherent timeline.

Trace spine
gateway.requesttenant_id
control-plane: run dispatchrun_id
model-router: routestep_id · model_used · tokens · cost_usd
runtime-manager: spawnvm_id · image · isolation_class
harness: step loopstep_id · tool_calls · reasoning_tokens · cache_tokens
W3C traceparent propagated at every boundary · durable resume re-joins the same trace_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

Every span emitted anywhere in the stack uses these 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
Agent namelantern.agent_nameInline executor + model-router
VM ID (microVM)vm_idScheduler / manager spans
Isolation class (microVM)isolation_classManager spans
Agent version (microVM)agent_versionManager 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)agent_name, node_type, tier, outcomeWall-clock duration of one workflow step, including all retry backoff. outcome is ok, failed, or retried.
lantern.run.step.retriesCounteragent_name, node_type, tierExtra attempts beyond the first. Only incremented when retryCount > 0.
lantern.run.replay.skipsCounteragent_nameCompletedStep cache hits during crash-resume. Each hit means one node was skipped rather than re-executed.
lantern.run.budget.blocksCounteragent_nameRuns denied by the agent's hard-fail budget policy (HTTP 402).
lantern.run.totalCounteragent_name, tier, statusTotal runs dispatched through the inline executor. tier is shared or microvm; status is succeeded or failed.

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-routerLANTERN_MODEL_ROUTER_ADDR (only when LANTERN_USE_MODEL_ROUTER=1 or addr is non-default)
  • runtime-schedulerLANTERN_SCHEDULER_GRPC_ADDR
  • runtime-managerLANTERN_DEFAULT_MANAGER_ADDR
  • workflow-engineLANTERN_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

Eight operator runbooks cover every active alert plus the DB restore procedure, linked from each alert's runbook: annotation in alerts.yml. Grafana dashboards: grafana/platform-overview.json and grafana/data-plane-runtime.json.