@lunora/observability

Host-neutral telemetry — logs, metrics, traces, request logs, and issue grouping.

@lunora/observability is the telemetry layer extracted out of @lunora/do: the in-memory ring buffers behind the Studio panels, the durable per-shard tables that survive hibernation, and the folding logic that turns raw events into what an operator reads.

Like the engine, it is host-neutral — it takes a SqlExec and plain event objects. @lunora/do wires it into ctx.log / ctx.trace / ctx.metrics and the __lunora_admin__ read RPCs, so an app gets it without importing it.

import { recordFunctionMetric, readFunctionMetrics } from "@lunora/observability";

What it provides

AreaWhat it does
LogsLogBuffer ring + durable request log; ctx.log lines and synthetic sources
TracesSpanBuffer + foldTraces — groups spans by trace, nests by parent, offsets from the root
MetricsMetricBuffer for the live readout, metric-history for the durable minute-bucketed series
Function statsfunction-metrics — durable per-function counters, latency, conflicts, and scan attribution
Query insightsquery-metrics — normalized statements, slow-query buckets
Issueserror grouping over @lunora/fingerprint, plus triage state
Issue explainerexplainIssue — grounds a folded issue in the error catalog, optionally rewritten by Workers AI
Auth & securityauth-metrics, security-audit
Correlationdatabase-telemetry, storage-correlation, trace-context

Two design notes

Durable and in-memory are different questions. The ring buffers answer "what has happened since this instance woke", which is what a live panel wants and what resets on a cold start. The reserved __lunora_* tables answer "what has happened to this shard", which is what an operator wants and what has to survive hibernation. Most areas ship both, and the read paths create their tables defensively so a panel opened before any traffic returns empty rather than throwing.

Everything caller-supplied is capped. A functionPath is attacker-reachable — an unregistered dispatch still records under the name the caller supplied — so the accumulator caps distinct paths, and the read paths clamp rows so a bloated table cannot materialize into a Durable Object's ~128MB isolate. The issue explainer caps every field that reaches a model prompt and fences the untrusted error report inside an explicit delimiter the system prompt names as a data boundary.

Degradation is the default

The explainer never fails a call: with no AI binding, a model error, or a timeout it returns a degraded: true result carrying the grounded catalog hint, so the client always has something to render. The AI layer is additive, never the only help.