@lunora/observability 0.0.0 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1968 @@
1
+ import { SqlExec, FunctionScanAttribution, FunctionCallStat } from '@lunora/shard-engine';
2
+ /** Reserved single-row auth accumulator table. Auto-hidden from the data browser by the `__lunora` prefix. */
3
+ declare const AUTH_METRICS_TABLE = "__lunora_auth_metrics";
4
+ /** Reserved coarse time-series table: app-wide auth attempt/failure counts bucketed by a fixed window. */
5
+ declare const AUTH_METRICS_BUCKETS_TABLE = "__lunora_auth_metrics_buckets";
6
+ /**
7
+ * Width of one history bucket, in milliseconds. 60s gives a minute-resolution
8
+ * time series — fine-grained enough to chart a burst of failed sign-ins on the
9
+ * studio, coarse enough that auth emits at most one row per minute. Mirrors
10
+ * `FUNCTION_METRICS_BUCKET_MS`.
11
+ */
12
+ declare const AUTH_METRICS_BUCKET_MS = 6e4;
13
+ /**
14
+ * Most recent buckets kept; older rows are trimmed after each write so the time
15
+ * series can't grow unbounded. 1440 minute-buckets ≈ 24h of auth history.
16
+ */
17
+ declare const AUTH_METRICS_BUCKET_RETENTION = 1440;
18
+ /** One coarse time-series sample: auth attempt/failure counts within `[bucketMs, bucketMs + AUTH_METRICS_BUCKET_MS)`. */
19
+ interface AuthMetricsBucket {
20
+ /** Auth attempts recorded in this window. */
21
+ attempts: number;
22
+ /** Epoch-ms floor of the bucket window. */
23
+ bucketMs: number;
24
+ /** Subset of `attempts` that failed (HTTP ≥ 400). */
25
+ failures: number;
26
+ }
27
+ /**
28
+ * Lifetime auth health for the app, served by `__lunora_admin__:getAuthMetrics`
29
+ * and consumed by the studio SLO panel. `failureRate` is the derived
30
+ * `failures / attempts` (0 when there have been no attempts), surfaced so the
31
+ * panel needn't recompute it; `sinceMs` is the epoch-ms the first attempt was
32
+ * recorded (a best-effort "since" marker); `history` is the minute-bucketed
33
+ * series for the sparkline, oldest bucket first.
34
+ */
35
+ interface AuthMetrics {
36
+ /** Total auth attempts (sign-in / sign-up / callback) recorded. */
37
+ attempts: number;
38
+ /** Derived `attempts === 0 ? 0 : failures / attempts`. */
39
+ failureRate: number;
40
+ /** Subset of `attempts` that failed (the auth route answered HTTP ≥ 400). */
41
+ failures: number;
42
+ /** Minute-bucketed attempt/failure series for the sparkline, oldest bucket first. */
43
+ history: AuthMetricsBucket[];
44
+ /** Epoch-ms the first attempt was recorded, or `0` on a never-seen app. */
45
+ sinceMs: number;
46
+ }
47
+ /** Fields recorded for one auth attempt. `outcome === "fail"` advances the failure counters. */
48
+ interface RecordAuthEventInput {
49
+ /** `"ok"` for a 2xx/3xx auth response, `"fail"` for an HTTP ≥ 400 response. */
50
+ outcome: "fail" | "ok";
51
+ /** Epoch-ms the auth attempt completed. */
52
+ ts: number;
53
+ }
54
+ /**
55
+ * Create the two reserved auth-metrics tables. Idempotent, so the read and write
56
+ * paths can call it defensively. The accumulator is a single keyed row; the
57
+ * bucket table is keyed by `bucket_ms` (one row per minute window).
58
+ */
59
+ declare const ensureAuthMetricsTables: (sql: SqlExec) => void;
60
+ /**
61
+ * Persist one auth attempt: a single upsert into the accumulator row and one
62
+ * upsert into the current time bucket, then a bounded trim of old buckets.
63
+ * Creates the tables first so callers needn't. `attempts` always advances;
64
+ * `failures` advances only when `outcome === "fail"`. `since_ms` is set once on
65
+ * the first attempt and never moved (so it stays a true first-seen marker).
66
+ *
67
+ * Exactly two `INSERT … ON CONFLICT … DO UPDATE` statements plus a bounded
68
+ * `DELETE`, all keyed by primary key — cheap enough to fire off the auth
69
+ * response path without blocking it.
70
+ */
71
+ declare const recordAuthEvent: (sql: SqlExec, input: RecordAuthEventInput) => void;
72
+ /**
73
+ * Read the durable auth metrics as the {@link AuthMetrics} wire shape the
74
+ * studio SLO panel consumes. Creates the tables first so a read on a
75
+ * never-authenticated app returns an all-zero shape (empty `history`) instead of
76
+ * throwing. `failureRate` is derived here so the consumer needn't recompute it.
77
+ */
78
+ declare const readAuthMetrics: (sql: SqlExec) => AuthMetrics;
79
+ /**
80
+ * Shared, bundler-inlined helpers for the structured `fields` a
81
+ * `ctx.log.<level>(message, fields)` / `ctx.log.with(fields)` call carries.
82
+ *
83
+ * Inlined (like {@link file://./otlp.ts}) so `@lunora/do`, `@lunora/runtime`,
84
+ * `@lunora/config`, and `@lunora/studio` — which sit on different tiers with no
85
+ * acceptable runtime dependency edge between them — share ONE implementation of
86
+ * field rendering/normalization instead of the byte-identical copies they would
87
+ * otherwise hand-mirror. Keep this genuinely zero-dependency (only built-ins) so
88
+ * inlining into each `dist` stays sound.
89
+ */
90
+ /** Structured, filterable key/value fields attached to a `ctx.log` line. */
91
+ type LogFields = Record<string, unknown>;
92
+ /**
93
+ * What kind of instrument produced a measurement, which decides how a collector
94
+ * aggregates it:
95
+ *
96
+ * - `counter` — a monotonic delta to add up (requests, retries, bytes sent).
97
+ * - `gauge` — a point-in-time reading that replaces the last one (queue depth,
98
+ * cache size).
99
+ * - `histogram` — a value whose *distribution* matters (latency, payload size),
100
+ * giving percentiles rather than just a mean.
101
+ */
102
+ type MetricKind = "counter" | "gauge" | "histogram";
103
+ /**
104
+ * One measurement recorded from a function handler.
105
+ *
106
+ * Each `ctx.metrics.*` call produces exactly one of these — the runtime does no
107
+ * pre-aggregation, so counters carry **delta** temporality and a collector sums
108
+ * them. That keeps the sink model identical to logs and spans (one event, one
109
+ * export) at the cost of chattiness in a hot loop, where the handler should sum
110
+ * locally and record once.
111
+ */
112
+ interface MetricEvent {
113
+ /**
114
+ * Structured attributes the caller attached, normalized to a fresh bag of
115
+ * JSON-safe primitives (see `shared/log-fields.ts`). These are the metric's
116
+ * dimensions — keep them low-cardinality; an id-valued attribute creates a
117
+ * distinct time series per id.
118
+ *
119
+ * Caller-controlled, so they MAY contain user input and they DO egress to
120
+ * whatever destination the sink ships to — the same caveat as a log line's
121
+ * `fields` and a span's `error.message`. Scrub upstream if that matters.
122
+ */
123
+ attributes?: LogFields;
124
+ /** Function path that recorded the measurement, e.g. `"orders:checkout"`. */
125
+ functionPath: string;
126
+ /** Instrument kind; see {@link MetricKind}. */
127
+ kind: MetricKind;
128
+ /** Instrument name, e.g. `"orders.placed"`. */
129
+ name: string;
130
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
131
+ shardKey?: string;
132
+ /**
133
+ * Trace id of the dispatch that recorded this measurement, when it ran inside
134
+ * one — the measurement's **exemplar**, letting a consumer jump from a metric
135
+ * point to a trace that produced it (OpenTelemetry's exemplar model). Stamped
136
+ * by the shard from the current request's trace context, not by the caller.
137
+ */
138
+ traceId?: string;
139
+ /** Wall-clock millis when the measurement was recorded. */
140
+ ts: number;
141
+ /**
142
+ * The measured value: the increment for a `counter`, the current reading for
143
+ * a `gauge`, the observed sample for a `histogram`.
144
+ */
145
+ value: number;
146
+ }
147
+ /** One eval verdict to turn into `gen_ai.evaluation.*` attributes. */
148
+ interface EvaluationInput {
149
+ /**
150
+ * Optional categorical label (e.g. `"pass"` / `"fail"` / a rubric bucket),
151
+ * emitted as the `.label` attribute. Omitted → no label attribute.
152
+ */
153
+ label?: string;
154
+ /**
155
+ * The scorer/evaluation name — becomes the key's name segment. Any character
156
+ * outside `[A-Za-z0-9._-]` is replaced with `_` so a scorer name carrying a
157
+ * colon (e.g. `"contains:shipped"`) still yields a well-formed attribute key.
158
+ */
159
+ name: string;
160
+ /** The numeric score (typically `[0, 1]`), emitted as the `.score` attribute. */
161
+ score: number;
162
+ }
163
+ /**
164
+ * Severity of a `ctx.log.*` call. The five console method names (`log` is the
165
+ * default level, distinct from `info`) plus `trace`/`fatal`, so the logger spans
166
+ * the full OpenTelemetry severity ramp (`trace`→`fatal`).
167
+ */
168
+ type ContextLogLevel = "debug" | "error" | "fatal" | "info" | "log" | "trace" | "warn";
169
+ /**
170
+ * One application log line emitted from a function handler via `ctx.log`.
171
+ * Produced per `ctx.log.*` call (unlike a per-dispatch RPC summary).
172
+ */
173
+ interface LogEvent {
174
+ /** Raw arguments passed to the `ctx.log.*` call, in order. */
175
+ args: unknown[];
176
+ /**
177
+ * OTel `LogRecord.eventName` — set when the line was emitted as a **structured
178
+ * event** via `ctx.log.event(name, fields)` rather than as a human-readable
179
+ * log line.
180
+ *
181
+ * The distinction is the whole point of the Events API: a log line's payload
182
+ * is its `message` (prose, for a human, unstable), while an event's payload is
183
+ * its `fields` (a named schema, for a query, stable). A collector that knows
184
+ * `eventName` can index and aggregate the latter; without it, "how many
185
+ * checkouts failed" degrades into a substring search over prose.
186
+ *
187
+ * Absent for ordinary `ctx.log.*` calls.
188
+ */
189
+ eventName?: string;
190
+ /**
191
+ * Structured fields the caller attached (`ctx.log.info(message, fields)` or a
192
+ * bound `ctx.log.with(fields)` child), already normalized to a fresh bag of
193
+ * JSON-safe primitives (see `shared/log-fields.ts`). Absent for a plain
194
+ * console-style call.
195
+ */
196
+ fields?: LogFields;
197
+ /** Function path that emitted the line, e.g. `"messages:list"`. */
198
+ functionPath: string;
199
+ /** Severity the line was logged at. */
200
+ level: ContextLogLevel;
201
+ /** Display string — the message, or the console-style args rendered and space-joined. */
202
+ message: string;
203
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
204
+ shardKey?: string;
205
+ /** Span id of the RPC this line was emitted under (trace correlation), or absent. */
206
+ spanId?: string;
207
+ /** Trace id this line belongs to (from the inbound `traceparent`), or absent. */
208
+ traceId?: string;
209
+ /** Wall-clock millis when the line was emitted. */
210
+ ts: number;
211
+ /** Acting userId, or absent when anonymous. */
212
+ userId?: string;
213
+ }
214
+ /**
215
+ * The OTel `SpanKind` union, in the spec's own words rather than its wire
216
+ * numbers, so a call site reads `{ kind: "client" }` instead of `{ kind: 3 }`.
217
+ *
218
+ * Kind is not cosmetic: a service map is built from it. A CLIENT span with no
219
+ * matching SERVER span on the other side is a dropped hop; PRODUCER/CONSUMER is
220
+ * what makes a queue render as an async edge rather than a synchronous call.
221
+ * Getting it wrong is why "everything is INTERNAL" traces produce no topology.
222
+ */
223
+ type OtlpSpanKind = "client" | "consumer" | "internal" | "producer" | "server";
224
+ /**
225
+ * One timestamped occurrence inside a span — OTel's `Span.events`.
226
+ *
227
+ * The right shape for something that has a moment but no duration: a retry, a
228
+ * cache miss, a validation failure, a thrown exception. Modelling those as
229
+ * near-zero-width child spans clutters the waterfall, and modelling them as
230
+ * separate log lines loses the "which span was I in" correlation that makes them
231
+ * useful in the first place.
232
+ */
233
+ interface SpanEventPoint {
234
+ /** Structured attributes, normalized like a span's own. */
235
+ attributes?: LogFields;
236
+ /** Event name, e.g. `"exception"` or `"cache.miss"`. */
237
+ name: string;
238
+ /** Wall-clock millis when it happened. */
239
+ ts: number;
240
+ }
241
+ /**
242
+ * A causal reference to a span in ANOTHER trace — OTel's `Span.links`.
243
+ *
244
+ * The standard answer to fan-in: a queue consumer processing a batch of 100
245
+ * messages links to the 100 producing spans rather than parenting to one of them
246
+ * (arbitrary) or all of them (impossible). The traces stay separately navigable
247
+ * and the causal edge survives.
248
+ */
249
+ interface SpanLink {
250
+ /** Attributes describing the relationship, e.g. `{ "link.kind": "enqueued_by" }`. */
251
+ attributes?: LogFields;
252
+ /** Linked span id (16-hex). */
253
+ spanId: string;
254
+ /** Linked trace id (32-hex). */
255
+ traceId: string;
256
+ }
257
+ /**
258
+ * One span produced by a `ctx.trace(name, fn)` call, or the synthetic root span
259
+ * the shard records for the dispatch itself so a waterfall has a bar to hang
260
+ * its children under.
261
+ *
262
+ * Ids are the same lowercase-hex form the OTLP encoders and `traceparent` use
263
+ * (32-hex trace, 16-hex span), so a `SpanEvent` composes into an OTLP span with
264
+ * no reformatting.
265
+ */
266
+ /**
267
+ * Handle the enclosing `ctx.trace` span hands its body, so the body can attach
268
+ * attributes only known *after* it resolves — an AI call's token usage or dollar
269
+ * cost, a downstream response's status, a computed row count. The start
270
+ * attributes passed to `ctx.trace(name, fn, attributes)` are snapshotted before
271
+ * the body runs (so a mid-span mutation can't rewrite them); anything set through
272
+ * this handle is merged over that snapshot at record time, with the post-hoc
273
+ * value winning on a key clash.
274
+ */
275
+ interface SpanHandle {
276
+ /**
277
+ * Record a timestamped {@link SpanEventPoint} on the enclosing span — a retry,
278
+ * a cache miss, a state transition. Prefer this over an extra `ctx.log` line
279
+ * for anything that only makes sense *relative to this span*: it rides the
280
+ * span's own export, so it costs no additional log record and can never be
281
+ * separated from its context.
282
+ */
283
+ addEvent: (name: string, attributes?: LogFields) => void;
284
+ /**
285
+ * Link this span to one in another trace (see {@link SpanLink}) — how a batch
286
+ * consumer points back at the requests that enqueued its items without
287
+ * collapsing every producer into one giant trace.
288
+ */
289
+ addLink: (link: SpanLink) => void;
290
+ /**
291
+ * Attach an AI **evaluation** verdict to this (generation) span as the
292
+ * `gen_ai.evaluation.<name>.score` / `.label` OpenTelemetry attributes, so a
293
+ * scorer's grade rides the same trace as the generation it graded and the
294
+ * collector reads it straight off the span. Convenience over
295
+ * {@link SpanHandle.setAttributes} that owns the key format; privacy-safe —
296
+ * only the name, score, and optional label are emitted, never the graded
297
+ * prompt or completion. Throws on an empty name or a non-finite score.
298
+ */
299
+ recordEvaluation: (evaluation: EvaluationInput) => void;
300
+ /**
301
+ * Record a caught exception as the OTel-conventional `exception` span event
302
+ * (`exception.type` / `exception.message` / `exception.stacktrace`).
303
+ *
304
+ * Distinct from letting the error propagate: this is for an error you
305
+ * **handled** — a retried request, a fallback that worked — which should be
306
+ * visible in the trace without marking the span failed. An error that escapes
307
+ * the span body is recorded automatically and *does* set the error status.
308
+ */
309
+ recordException: (error: unknown) => void;
310
+ /** Set one attribute on the enclosing span (merged at record time; post-hoc wins on key clash). */
311
+ setAttribute: (key: string, value: LogFields[string]) => void;
312
+ /** Merge attributes onto the enclosing span (post-hoc wins on key clash). */
313
+ setAttributes: (fields: LogFields) => void;
314
+ /**
315
+ * The W3C ids of the span this handle refers to.
316
+ *
317
+ * A handle that cannot say WHICH span it is forces every consumer that needs
318
+ * the identity — a `traceparent` for a hand-rolled outbound call, a trace id
319
+ * echoed in an error response so a user can quote it in a bug report, an
320
+ * `@opentelemetry/api` bridge parenting a third-party library's spans — to
321
+ * reach around the API for it.
322
+ */
323
+ spanContext: () => {
324
+ spanId: string;
325
+ traceId: string;
326
+ };
327
+ }
328
+ /** Options accepted by `ctx.trace(name, fn, options)` beyond the plain attribute bag. */
329
+ interface SpanOptions {
330
+ /** Start attributes, snapshotted before the body runs. */
331
+ attributes?: LogFields;
332
+ /**
333
+ * OTel `SpanKind`, default `"internal"`. Set `"client"` for a call OUT to
334
+ * another service, `"producer"`/`"consumer"` for queue hops — this is what a
335
+ * collector builds its service map from, so leaving everything `"internal"`
336
+ * yields a trace with no topology.
337
+ */
338
+ kind?: OtlpSpanKind;
339
+ /** Links to spans in other traces, known at start (see {@link SpanLink}). */
340
+ links?: SpanLink[];
341
+ }
342
+ interface SpanEvent {
343
+ /**
344
+ * Structured attributes the caller attached, already normalized to a fresh
345
+ * bag of JSON-safe primitives (see `shared/log-fields.ts`) exactly like a log
346
+ * line's `fields`. Absent when the caller passed none.
347
+ */
348
+ attributes?: LogFields;
349
+ /** Wall-clock duration of the span body, in milliseconds. */
350
+ durationMs: number;
351
+ /**
352
+ * Timestamped occurrences inside the span (see {@link SpanEventPoint}) —
353
+ * `ctx.trace`'s `span.addEvent(...)` / `span.recordException(...)`. Absent
354
+ * when the body recorded none.
355
+ */
356
+ events?: SpanEventPoint[];
357
+ /**
358
+ * Populated when the span body threw. `type` is the error's constructor name
359
+ * (or its `LunoraError` code); `message` is the human-readable string and may
360
+ * include user input, so sinks shipping to third parties should scrub it.
361
+ */
362
+ error?: {
363
+ message: string;
364
+ type: string;
365
+ };
366
+ /**
367
+ * Function path the span was created under, e.g. `"messages:list"`. A span
368
+ * created inside a function invoked via `ctx.runQuery`/`runMutation`/
369
+ * `runAction` carries the OUTER entrypoint's path, since the composed call
370
+ * reuses its context — the same attribution rule `ctx.log` follows.
371
+ */
372
+ functionPath: string;
373
+ /**
374
+ * OTel `SpanKind`. Absent means `"internal"` — the overwhelming majority of
375
+ * `ctx.trace` spans — so the common case costs no bytes on the wire and every
376
+ * pre-existing recorded span stays valid.
377
+ */
378
+ kind?: OtlpSpanKind;
379
+ /** Causal references to spans in other traces (see {@link SpanLink}). Absent when none. */
380
+ links?: SpanLink[];
381
+ /** Caller-supplied span name, e.g. `"stripe.charge"`. */
382
+ name: string;
383
+ /** True when the span body returned without throwing. */
384
+ ok: boolean;
385
+ /**
386
+ * Span id of the enclosing span — the parent `ctx.trace` when nested, else
387
+ * the dispatch's own RPC span (from the inbound `traceparent`). A span with
388
+ * no inbound trace context is parented to a locally-minted root, so this is
389
+ * always set for a `ctx.trace` span; only the synthetic `dispatch` span below
390
+ * carries `""`, meaning "nothing above me in this trace".
391
+ */
392
+ parentSpanId: string;
393
+ /**
394
+ * True for the synthetic span representing the **dispatch itself**, which the
395
+ * shard records so a waterfall has a bar for the request to hang its
396
+ * `ctx.trace` spans under.
397
+ *
398
+ * Named for what it is rather than "root": it is not the root of the
399
+ * collector-side trace — the worker's own RPC span sits above it — and it is
400
+ * never exported to a sink, because the runtime already emits that dispatch
401
+ * via `onRpc` and a collector would otherwise show it twice. Locally it *is*
402
+ * the outermost span, which is why the fold prefers it as a trace's anchor.
403
+ */
404
+ dispatch?: boolean;
405
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
406
+ shardKey?: string;
407
+ /** This span's own id (16-hex). */
408
+ spanId: string;
409
+ /** Wall-clock millis when the span started. */
410
+ startTs: number;
411
+ /** Trace this span belongs to (32-hex) — shared with the dispatch's logs. */
412
+ traceId: string;
413
+ /** Acting userId, or absent when anonymous. */
414
+ userId?: string;
415
+ }
416
+ /**
417
+ * Structural shape of the `ctx.trace` span factory (see the server
418
+ * `LunoraTracer`). Declared here rather than imported so `@lunora/do` takes no
419
+ * dependency on `@lunora/server`; a cross-package assignability guard in
420
+ * `@lunora/testing` fails the build if the two drift apart.
421
+ *
422
+ * The body's second argument is the enclosing span's {@link SpanHandle}, through
423
+ * which it can attach attributes only known *after* it resolves (post-hoc). It is
424
+ * a trailing parameter, so a `(trace) => …` body that ignores it still conforms.
425
+ */
426
+ type ContextTracer = <T>(name: string, function_: (trace: ContextTracer, span: SpanHandle) => Promise<T> | T, options?: LogFields | SpanOptions) => Promise<T>;
427
+ /** Structural shape of the `ctx.metrics` recorder (see the server `LunoraMetrics`). */
428
+ interface ContextMetrics {
429
+ count: (name: string, value?: number, attributes?: LogFields) => void;
430
+ gauge: (name: string, value: number, attributes?: LogFields) => void;
431
+ record: (name: string, value: number, attributes?: LogFields) => void;
432
+ }
433
+ /** The trace a ctx's spans hang off: the shared id, and the span they parent to. */
434
+ interface TraceAnchor {
435
+ rootSpanId: string;
436
+ /**
437
+ * The W3C `sampled` verdict for this trace, inherited from the inbound
438
+ * `traceparent` when there was one. Carried on the anchor rather than
439
+ * re-derived per outbound call so every `ctx.fetch` of one dispatch
440
+ * propagates the same answer.
441
+ */
442
+ sampled?: boolean;
443
+ traceId: string;
444
+ }
445
+ /**
446
+ * Minimal structural shape of one **host-native custom span** — the object a
447
+ * `tracing.enterSpan(name, (span) => …)` callback receives (GA 2026-06-16). Only
448
+ * the surface the bridge touches is declared, so `@lunora/do` needs no runtime
449
+ * dependency on `cloudflare:workers`; the real platform span is structurally
450
+ * assignable.
451
+ */
452
+ /**
453
+ * A host-supplied span, structurally.
454
+ *
455
+ * Named for the role rather than the provider: this is whatever the runtime's
456
+ * own tracer hands back, and Cloudflare's `enterSpan` callback argument is one
457
+ * shape that satisfies it. Kept structural so no provider type is imported —
458
+ * the repo's documented `*Like` pattern.
459
+ */
460
+ interface HostSpanLike {
461
+ /**
462
+ * Whether this span is actually being recorded by the runtime's sampler.
463
+ * `false` off the traced path (unsampled) — the bridge skips its
464
+ * `setAttribute` work in that case rather than building attribute strings for
465
+ * a span nobody will read.
466
+ */
467
+ readonly isTraced: boolean;
468
+ /** Attach one primitive attribute to the CF span. */
469
+ setAttribute: (key: string, value: boolean | number | string | undefined) => void;
470
+ }
471
+ /**
472
+ * Minimal structural shape of the `tracing` namespace exported by
473
+ * `cloudflare:workers`. `enterSpan` opens a custom span that auto-nests under the
474
+ * runtime's ambient span and ends when `callback` settles.
475
+ */
476
+ interface HostTracingLike {
477
+ enterSpan: <T>(name: string, callback: (span: HostSpanLike) => T) => T;
478
+ }
479
+ /**
480
+ * Resolves CF's `tracing` namespace, or `undefined` when it is unavailable —
481
+ * on a host with no native tracer, on a Cloudflare compat date predating custom spans, or when
482
+ * `tracing.enterSpan` is not a function. **Injected, never imported here**, so the
483
+ * tracer stays pure and unit-testable without `cloudflare:workers`; the shard
484
+ * supplies the real resolver, tests a fake or `undefined`.
485
+ */
486
+ type HostTracingResolver = () => HostTracingLike | Promise<HostTracingLike | undefined> | undefined;
487
+ /** What {@link createTracer} needs from the shard to build a span. */
488
+ interface TracerDeps {
489
+ /** The trace this ctx's spans belong to. */
490
+ anchor: TraceAnchor;
491
+ /**
492
+ * Whether to record a span body's error message (and a `recordException`
493
+ * stacktrace) verbatim rather than redacted. Mirrors the request log's
494
+ * `captureRaw`: `true` in dev (`isDevEnvironment`), `false` in production —
495
+ * the span pipeline is the one sink third-party collectors (Datadog/Axiom
496
+ * via `otlpSink`) receive, so it must not ship raw PII/internals by default
497
+ * the way the request log and function-metrics sinks already don't.
498
+ */
499
+ captureRaw?: boolean;
500
+ /** Function path the spans are attributed to. */
501
+ functionPath: string;
502
+ /**
503
+ * **Opt-in, EXPERIMENTAL, default off.** When `true` *and*
504
+ * {@link TracerDeps.resolveHostTracing} yields a working
505
+ * `tracing.enterSpan`, each `ctx.trace` span is ALSO emitted as a host-native
506
+ * **custom span**, so it nests inside CF's native binding/fetch/handler trace
507
+ * tree on the hosted path. This only ADDS a CF-side span — the recorded
508
+ * {@link SpanEvent} (our `SpanBuffer`/`otlpSink`) is untouched and remains the
509
+ * source of truth plus the local studio waterfall. The `enterSpan` call itself
510
+ * is now workerd-validated as available and side-effect-free inside a Durable
511
+ * Object; CF's exported parent-linking under sampling remains unverified. See
512
+ * {@link createTracer} for the double-export and Durable-Object async-context
513
+ * caveats.
514
+ */
515
+ fuseHostSpans?: boolean;
516
+ /** Hand a finished span to the buffer + sink. */
517
+ record: (span: SpanEvent) => void;
518
+ /**
519
+ * Injected resolver for CF's `tracing` namespace (see
520
+ * {@link HostTracingResolver}). Only consulted when
521
+ * {@link TracerDeps.fuseHostSpans} is `true`, so the default path never
522
+ * calls it.
523
+ */
524
+ resolveHostTracing?: HostTracingResolver;
525
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
526
+ shardKey: string | undefined;
527
+ /** Read lazily — the acting user is resolved per span, not per ctx. */
528
+ userId: () => string | undefined;
529
+ }
530
+ /** What {@link createMetrics} needs from the shard to build a measurement. */
531
+ interface MetricsDeps {
532
+ functionPath: string;
533
+ record: (event: MetricEvent) => void;
534
+ shardKey: string | undefined;
535
+ }
536
+ /** Everything a {@link SpanHandle}'s body attached, ready to merge into the recorded span. */
537
+ interface SpanCollection {
538
+ attributes: Record<string, LogFields[string]>;
539
+ events: SpanEventPoint[];
540
+ links: SpanLink[];
541
+ }
542
+ /** A {@link SpanHandle} plus read access to what it has collected so far. */
543
+ interface SpanCollector {
544
+ collected: SpanCollection;
545
+ handle: SpanHandle;
546
+ }
547
+ /**
548
+ * Build a span's post-hoc collection surface: the {@link SpanHandle} handed to a
549
+ * `ctx.trace` body, and the bag it writes into.
550
+ *
551
+ * Factored out because the same surface backs two things — every `ctx.trace`
552
+ * span, and the per-dispatch **wide event** (`ctx.span`), where the accumulated
553
+ * attributes become the canonical one-event-per-request summary. Sharing the
554
+ * implementation is what makes those two feel like the same API instead of two
555
+ * that happen to resemble each other.
556
+ *
557
+ * `captureRaw` (default `false`) gates `recordException`'s `exception.message`/
558
+ * `exception.stacktrace` the same way {@link createTracer} gates a span's own
559
+ * error message — a stack is file paths and internals by definition, the exact
560
+ * class `isInternalCode` redaction exists for, so it rides the same dev-only
561
+ * escape hatch rather than shipping to a third-party collector by default.
562
+ */
563
+ declare const createSpanCollector: (ids: {
564
+ spanId: string;
565
+ traceId: string;
566
+ }, captureRaw?: boolean) => SpanCollector;
567
+ /**
568
+ * Build the `ctx.trace` span factory for one dispatched function.
569
+ *
570
+ * **Nesting is explicit, not ambient.** Each span's body receives a tracer bound
571
+ * to that span; calling it is what makes a child. An earlier design kept an
572
+ * ambient stack of "the currently open span" and parented to its top, which
573
+ * reads nicer but is unfixably wrong under concurrency: in
574
+ * `Promise.all([trace("a", …), trace("b", …)])`, `b` starts while `a` is on the
575
+ * stack and is recorded as a *child* of `a` rather than its sibling — and
576
+ * parallel fan-out is one of the main things people reach for a tracer to
577
+ * measure. Distinguishing "called inside a's body" from "called concurrently
578
+ * with a" needs `AsyncLocalStorage`, which this package deliberately avoids (see
579
+ * `dependency-tracker.ts` — shard DOs run under a slimmer compat profile than
580
+ * `nodejs_compat`). So the parent is threaded, exactly like the dependency
581
+ * tracker and the subscription identity: correct in every case, and visible at
582
+ * the call site.
583
+ *
584
+ * The anchor is passed in for the same reason. `ShardDO.currentRequestTrace` is
585
+ * cleared in the dispatch `finally`, and a subscription re-run builds its ctx
586
+ * during* the writing mutation's flush — so reading that shared field at span
587
+ * time would file the re-run's spans under the mutation's trace.
588
+ *
589
+ * **Cloudflare custom-spans bridge (opt-in, EXPERIMENTAL).** When
590
+ * `deps.fuseHostSpans` is `true` and `deps.resolveHostTracing` yields
591
+ * a working `tracing.enterSpan` (`cloudflare:workers`, GA 2026-06-16), each span
592
+ * body runs inside a CF custom span so our span nests under CF's native
593
+ * binding/fetch/handler trace tree on the hosted path, and the finished span's
594
+ * key attributes are mirrored onto it (gated on `span.isTraced`). Two deliberate
595
+ * boundaries hold.
596
+ *
597
+ * **No double-export by default, and never a replacement.** The bridge only ADDS a
598
+ * CF-side span; the recorded {@link SpanEvent} handed to `record` (our
599
+ * `SpanBuffer`/`otlpSink`) is byte-for-byte the same as without the bridge and
600
+ * stays the source of truth. It is off unless explicitly enabled precisely
601
+ * because, once on, a deployment that ALSO ships our `otlpSink` to a collector
602
+ * AND lets CF export its trace tree emits the same logical span down two
603
+ * pipelines — an intentional, documented trade the operator opts into, not a
604
+ * default.
605
+ *
606
+ * **DO async-context caveat (EXPERIMENTAL, partially workerd-validated).**
607
+ * `tracing.enterSpan` is now confirmed to EXIST and RUN inside a real Durable
608
+ * Object under `@cloudflare/vitest-pool-workers` (see
609
+ * `__tests__/workerd/context-telemetry-cf-bridge.workerd.test.ts`): it resolves
610
+ * from `cloudflare:workers`, its callback executes and returns the body value
611
+ * without throwing, `span.isTraced` is a real boolean, and — the key additive
612
+ * guarantee — our recorded {@link SpanEvent} tree (parent/child via the threaded
613
+ * `parentSpanId`) is byte-for-byte identical with the bridge on vs off. What that
614
+ * harness CANNOT prove is CF's own *exported* parent-linking: with no trace head
615
+ * attached the run is unsampled (`isTraced === false`), so CF records nothing and
616
+ * its span tree is not introspectable. So `enterSpan`'s ambient-span parent-linking
617
+ * inside a DO stays unverified upstream, and this remains capability-probed: an
618
+ * absent/undefined `tracing`, a missing `enterSpan`, or an off-CF/unsampled run all
619
+ * resolve to `undefined`/no-op — exact prior behavior. If CF's ambient-span linkage
620
+ * misbehaves in a DO, the worst case is a mis-parented CF span; our own recorded
621
+ * waterfall is unaffected.
622
+ */
623
+ declare const createTracer: (deps: TracerDeps) => ContextTracer;
624
+ /** What {@link createTracedFetch} needs from the shard. */
625
+ interface TracedFetchDeps {
626
+ /** The trace the CLIENT spans belong to. */
627
+ anchor: TraceAnchor;
628
+ /** Function path the spans are attributed to. */
629
+ functionPath: string;
630
+ /**
631
+ * Whether to inject `traceparent` into the outbound request — and, with a
632
+ * predicate, to which destinations. Default `true`.
633
+ */
634
+ propagate?: ((url: URL) => boolean) | boolean;
635
+ /** Hand a finished span to the buffer + sink. */
636
+ record: (span: SpanEvent) => void;
637
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
638
+ shardKey: string | undefined;
639
+ /** Read lazily — the acting user is resolved per span. */
640
+ userId: () => string | undefined;
641
+ }
642
+ /** The `fetch` shape `ctx.fetch` exposes — the platform global's, narrowed to what we wrap. */
643
+ type ContextFetch = (input: Request | string | URL, init?: RequestInit) => Promise<Response>;
644
+ /**
645
+ * Build `ctx.fetch`: the platform `fetch`, wrapped so every outbound call
646
+ * becomes a **CLIENT span** and carries W3C trace context to the callee.
647
+ *
648
+ * Two gaps close here. First, an uninstrumented `fetch` makes the single most
649
+ * common source of latency — waiting on somebody else's service — invisible: a
650
+ * handler that spends 900ms in Stripe shows one opaque 900ms bar. Second,
651
+ * without an outbound `traceparent` the callee starts a brand-new trace, so the
652
+ * two halves of one logical request can never be stitched together, which is the
653
+ * entire premise of distributed tracing.
654
+ *
655
+ * The span id is minted BEFORE the request is sent, precisely so the header
656
+ * announces the id the span will actually be recorded under. Deriving it
657
+ * afterwards (or reusing the parent's) would produce a `traceparent` naming a
658
+ * span that never existed, and a callee parented to nothing.
659
+ *
660
+ * Kind is `client` rather than `internal` — that is what lets a collector draw
661
+ * the edge to the downstream service in a service map.
662
+ *
663
+ * Failures are recorded and re-thrown untouched, and a non-2xx response is
664
+ * recorded as an ERROR span (it is a failed call from the caller's point of
665
+ * view) while still being returned normally — instrumentation, never flow
666
+ * control.
667
+ */
668
+ declare const createTracedFetch: (deps: TracedFetchDeps, base: ContextFetch) => ContextFetch;
669
+ /**
670
+ * Build the `ctx.metrics` recorder for one dispatched function.
671
+ *
672
+ * Deliberately stateless: each call emits one measurement rather than
673
+ * accumulating into a per-dispatch map. Pre-aggregating here would have to pick a
674
+ * flush point and a merge rule per instrument kind (sum a counter, last-wins a
675
+ * gauge, and a histogram cannot be merged at all without losing the
676
+ * distribution) — so the runtime stays a transport and the collector, which is
677
+ * built for exactly this, does the aggregation.
678
+ */
679
+ declare const createMetrics: (deps: MetricsDeps) => ContextMetrics;
680
+ /**
681
+ * Build the synthetic root span for a finished dispatch — the bar the studio's
682
+ * waterfall hangs a request's `ctx.trace` spans under.
683
+ *
684
+ * Pure: the caller decides whether to record it (only when the dispatch actually
685
+ * produced spans) and where to put it. It is never routed to `sink.onSpan`,
686
+ * because the runtime already emits the dispatch to `onRpc` and a collector would
687
+ * otherwise show it twice.
688
+ */
689
+ declare const dispatchRootSpan: (input: {
690
+ anchor: TraceAnchor;
691
+ /**
692
+ * Whether to record the failure message verbatim rather than redacted —
693
+ * the same dev-only escape hatch as {@link TracerDeps.captureRaw}. This
694
+ * synthetic root span carries the SAME error message the request log and
695
+ * function-metrics sinks already redact by default, so it must not be the
696
+ * one durable copy that ships it raw to a third-party collector.
697
+ */
698
+ captureRaw?: boolean;
699
+ /**
700
+ * What the handler attached to the dispatch through `ctx.span` — the **wide
701
+ * event**. These are the attributes that would otherwise have been scattered
702
+ * across a dozen `ctx.log` lines; carrying them on the one span that already
703
+ * exists per request is the OTel-native way to get a wide event without
704
+ * multiplying log records.
705
+ */
706
+ collected?: SpanCollection;
707
+ durationMs: number;
708
+ failure: {
709
+ thrown: unknown;
710
+ } | undefined;
711
+ functionPath: string;
712
+ shardKey: string | undefined;
713
+ startTs: number;
714
+ userId: string | undefined;
715
+ }) => SpanEvent;
716
+ /** Running totals for `"summary"` mode; created by the caller, read once at the dispatch boundary. */
717
+ interface DatabaseTally {
718
+ calls: number;
719
+ durationMs: number;
720
+ errors: number;
721
+ perOperation: Record<string, number>;
722
+ spansEmitted: number;
723
+ spansTruncated: boolean;
724
+ }
725
+ /**
726
+ * How much detail `ctx.db` auto-instrumentation produces.
727
+ *
728
+ * `"summary"` (default) — aggregate counters on the dispatch's wide event: no
729
+ * extra spans, no extra log records, and a cost that does not grow with call count.
730
+ *
731
+ * `"spans"` — one span per database call. The full waterfall, at the price of a
732
+ * span per call; right when diagnosing, noisy as a permanent default.
733
+ *
734
+ * `"off"` — no database telemetry at all.
735
+ */
736
+ type DatabaseInstrumentation = "off" | "spans" | "summary";
737
+ /** What {@link instrumentDatabase} needs to record what it observes. */
738
+ interface DatabaseTelemetryDeps {
739
+ /** The trace produced spans belong to (`"spans"` mode only). */
740
+ anchor: TraceAnchor;
741
+ /**
742
+ * Whether to record a failed call's error message verbatim rather than
743
+ * redacted (`"spans"` mode only) — the same dev-only escape hatch as
744
+ * `TracerDeps.captureRaw`. A constraint-error message quotes the
745
+ * conflicting row, so this CLIENT span gets the same default-redacted
746
+ * posture as the request log and function-metrics sinks.
747
+ */
748
+ captureRaw?: boolean;
749
+ /** Function path spans and attributes are attributed to. */
750
+ functionPath: string;
751
+ /** Detail level; see {@link DatabaseInstrumentation}. */
752
+ mode: DatabaseInstrumentation;
753
+ /** Hand a finished span to the buffer + sink (`"spans"` mode only). */
754
+ record: (span: SpanEvent) => void;
755
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
756
+ shardKey: string | undefined;
757
+ /**
758
+ * Caller-supplied accumulator for `"summary"` mode. The instrumenter only ever
759
+ * increments numbers on it; the shard reads it ONCE at the dispatch boundary
760
+ * and formats it with {@link formatTally}.
761
+ *
762
+ * Two properties fall out of that split. Per-call cost stays at a few integer
763
+ * increments — no object allocation, no lookup — which matters because this is
764
+ * on the path of every query. And because nothing is written through the
765
+ * dispatch's `SpanHandle`, the wide-event collector is never materialized, so
766
+ * a handler that instrumented nothing still doesn't trip the root-span gate.
767
+ */
768
+ tally: DatabaseTally;
769
+ /** Read lazily — the acting user is resolved per span. */
770
+ userId: () => string | undefined;
771
+ }
772
+ /**
773
+ * Wrap a `ctx.db` writer so its storage-touching methods are instrumented.
774
+ *
775
+ * Returns the database unchanged when `mode` is `"off"`, so the default-disabled
776
+ * path costs nothing — not even a proxy indirection.
777
+ *
778
+ * Implemented as a `Proxy` rather than by enumerating and rebinding methods:
779
+ * `DatabaseWriterLike` has optional members that a given backend may or may not
780
+ * implement, plus properties (`system`) and builder factories (`query`) that
781
+ * must pass through untouched. A proxy instruments exactly what it is asked to
782
+ * and is transparently correct for everything else, including members added
783
+ * later — an enumeration would silently stop covering them.
784
+ */
785
+ declare const instrumentDatabase: <T extends object>(database: T, deps: DatabaseTelemetryDeps) => T;
786
+ /** A zero'd tally for one dispatch. */
787
+ declare const createDatabaseTally: () => DatabaseTally;
788
+ /**
789
+ * Render the running tally as span attributes.
790
+ *
791
+ * Called ONCE per dispatch, from the shard's root-span recorder — not per query.
792
+ * Building this object on every call was pure waste on a hot path. It is a handful of keys, so the per-call cost is a small object
793
+ * assignment — the property that makes `"summary"` mode scale to any call count.
794
+ */
795
+ declare const formatTally: (tally: DatabaseTally) => LogFields;
796
+ /** Reserved per-function accumulator table. Auto-hidden from the data browser by the `__lunora` prefix. */
797
+ declare const FUNCTION_METRICS_TABLE = "__lunora_metrics";
798
+ /** Reserved coarse time-series table: per-function call/error counts bucketed by a fixed window. */
799
+ declare const FUNCTION_METRICS_BUCKETS_TABLE = "__lunora_metrics_buckets";
800
+ /** Reserved causal full-scan attribution table: per-(function, table) full-scan counts. */
801
+ declare const FUNCTION_METRICS_SCANS_TABLE = "__lunora_metrics_scans";
802
+ /** Reserved per-(table, index) hit-counter table backing the advisor dead-index lint. */
803
+ declare const FUNCTION_METRICS_INDEX_TABLE = "__lunora_metrics_index";
804
+ /**
805
+ * Width of one history bucket, in milliseconds. 60s gives a minute-resolution
806
+ * time series — fine-grained enough to chart bursts on the studio, coarse
807
+ * enough that a single function emits at most one row per minute. Exported so
808
+ * consumers (and tests) can align timestamps to the same grid.
809
+ */
810
+ declare const FUNCTION_METRICS_BUCKET_MS = 6e4;
811
+ /**
812
+ * Most recent buckets kept per function; older rows are trimmed after each
813
+ * write so the time series can't grow unbounded. 1440 minute-buckets ≈ 24h of
814
+ * history per function.
815
+ */
816
+ declare const FUNCTION_METRICS_BUCKET_RETENTION = 1440;
817
+ /**
818
+ * Maximum distinct function `path`s tracked in the accumulator table. Mirrors
819
+ * `query-metrics.ts`'s `QUERY_METRICS_MAX_STATEMENTS` cap (and exists for the
820
+ * same reason): the real bound is the app's own registered-function set plus
821
+ * deploy churn (a rename/removal leaves its old path's row in place, still
822
+ * counted against the cap, until an operator's own retention/cleanup
823
+ * process — there is none built in today) — a few thousand registered
824
+ * functions is already far beyond any real app. `shard-do.ts`'s dispatch
825
+ * handler explicitly does NOT record per-function metrics for an
826
+ * unregistered/`FUNCTION_NOT_FOUND` dispatch (see the guard next to its
827
+ * `FUNCTION_NOT_FOUND` check), so a caller cannot mint arbitrary `path`s here
828
+ * the way a raw caller-supplied SQL shape can in `query-metrics.ts`. Without a
829
+ * cap, deploy churn across the app's lifetime would still grow
830
+ * `__lunora_metrics` (and its bucket/scan satellites) without bound,
831
+ * eventually filling the shard's SQLite store shared with the app's real
832
+ * data. At the cap, a brand-new path is refused — protecting the incumbent
833
+ * leaderboard from a flood of one-off paths is the point, so admission is
834
+ * refused rather than evicting an existing path to make room; already-tracked
835
+ * paths keep accumulating past the cap. `readFunctionMetricsTotals`'s
836
+ * `capped` is the read-side signal for this.
837
+ */
838
+ declare const FUNCTION_METRICS_MAX_PATHS = 5e3;
839
+ /**
840
+ * Upper bound on rows the admin reads materialize into DO memory at once. Even
841
+ * with the write-side `FUNCTION_METRICS_MAX_PATHS` cap in place, an existing
842
+ * shard could already hold a bloated accumulator (rows written before the cap
843
+ * landed), so the read path also clamps — a `SELECT *` with no LIMIT would
844
+ * otherwise load every row via `.toArray()` and risk OOMing the ~128MB DO when
845
+ * the studio Function Stats panel opens. Ordered reads keep the busiest/most
846
+ * recent rows; the tail past this limit is simply not returned to the panel.
847
+ */
848
+ declare const FUNCTION_METRICS_READ_LIMIT = 1e3;
849
+ /** One coarse time-series sample for a function: call/error counts within `[bucketMs, bucketMs + FUNCTION_METRICS_BUCKET_MS)`. */
850
+ interface FunctionMetricBucket {
851
+ /** Epoch-ms floor of the bucket window. */
852
+ bucketMs: number;
853
+ /** Dispatches recorded in this window. */
854
+ calls: number;
855
+ /** Subset of `calls` that threw. */
856
+ errors: number;
857
+ }
858
+ /** {@link readFunctionMetricBuckets} result: the time-series window plus whether the read limit cut it short. */
859
+ interface FunctionMetricBucketsResult {
860
+ buckets: (FunctionMetricBucket & {
861
+ path: string;
862
+ })[];
863
+ /**
864
+ * True when more rows existed than {@link FUNCTION_METRICS_READ_LIMIT} could
865
+ * return, so `buckets` is a partial (newest) window rather than the app's
866
+ * full retained history. Mirrors `readQueryInsights`'s `capped` and
867
+ * `foldTraces`'s `total`: a silently truncated read looks identical to a
868
+ * complete one to a caller that doesn't check for it — the Metrics chart's
869
+ * window would appear to shrink as the app grows, with a wrong leftmost
870
+ * bar, and nothing would say why.
871
+ */
872
+ truncated: boolean;
873
+ }
874
+ /** One declared index a dispatch exercised (used to narrow a read). */
875
+ interface IndexHit {
876
+ /** The declared index name. */
877
+ index: string;
878
+ /** The table the index is declared on. */
879
+ table: string;
880
+ }
881
+ /** One declared index's cumulative recorded read count (durable, non-decaying) — the advisor dead-index lint input. */
882
+ interface FunctionMetricIndexHit {
883
+ /** The declared index name. */
884
+ index: string;
885
+ /** Recorded reads that used this index to narrow. */
886
+ reads: number;
887
+ /** The table the index is declared on. */
888
+ table: string;
889
+ }
890
+ /** Fields recorded for one completed dispatch. `errored` advances the failure counters. */
891
+ interface RecordFunctionMetricInput {
892
+ /**
893
+ * Whether the dispatch failed on an optimistic-concurrency (OCC) write
894
+ * conflict — a compare-and-swap that lost to a concurrent commit. Advances
895
+ * the durable `conflicts` counter behind the write-contention advisor. A
896
+ * conflicted dispatch also `errored`, so conflicts are a subset of errors.
897
+ * Omitted/false on the common path, keeping the hot path unchanged.
898
+ */
899
+ conflicted?: boolean;
900
+ /** Wall-clock millis the handler took. */
901
+ durationMs: number;
902
+ /** Whether the dispatch threw. */
903
+ errored: boolean;
904
+ /** Most recent failure message, recorded only when `errored`. */
905
+ errorMessage?: string;
906
+ /**
907
+ * Distinct declared indexes this dispatch exercised (used to narrow a read),
908
+ * collected from the `onIndexUse` signal. Each entry bumps the
909
+ * per-`(table, index)` hit counter in `__lunora_metrics_index`, the durable
910
+ * producer behind the advisor dead-index lint. Omitted/empty when the
911
+ * dispatch used no declared index, keeping the hot path unchanged.
912
+ */
913
+ indexHits?: ReadonlyArray<IndexHit>;
914
+ /** The `<file>:<function>` identifier. */
915
+ path: string;
916
+ /**
917
+ * Distinct tables this dispatch full-scanned (read with no index / point
918
+ * lookup), collected from the `SCAN_DEP` reads. Each entry bumps the
919
+ * aggregate `scans` counter and the per-`(path, table)` attribution row.
920
+ * Omitted/empty when the dispatch didn't full-scan anything (the common
921
+ * indexed case), keeping the hot path to the same two upserts as before.
922
+ */
923
+ scannedTables?: ReadonlyArray<string>;
924
+ /** Epoch-ms the dispatch completed. */
925
+ ts: number;
926
+ }
927
+ /**
928
+ * Create the four reserved metrics tables. Idempotent, so the read and write
929
+ * paths can call it defensively. The accumulator table is keyed by `path` (one
930
+ * row per function); the bucket table by `(path, bucketMs)` (one row per
931
+ * function per window); the scans table by `(path, table)` (one row per
932
+ * function per full-scanned table); the index table by `(table, index)` (one
933
+ * row per declared index the app exercises).
934
+ *
935
+ * The accumulator's `scans` column is added via a guarded `ALTER TABLE` rather
936
+ * than baked into the `CREATE` so a shard whose `__lunora_metrics` predates the
937
+ * causal-attribution feature gains the column on the next call without a
938
+ * migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
939
+ * error from a re-run is swallowed. Both the `CREATE`s and the back-fill only
940
+ * run once per handle (see {@link ensuredHandles}) — a handle already marked
941
+ * ensured returns immediately.
942
+ */
943
+ declare const ensureFunctionMetricsTables: (sql: SqlExec) => void;
944
+ /**
945
+ * Persist one completed dispatch: a single upsert into the accumulator row and
946
+ * one upsert into the current time bucket, then a bounded trim of old buckets
947
+ * for that path. Creates the tables first so callers needn't. This is the hot
948
+ * path — exactly two `INSERT … ON CONFLICT … DO UPDATE` statements plus a
949
+ * bounded `DELETE`, all keyed by primary key, so it stays cheap.
950
+ *
951
+ * When the dispatch full-scanned one or more tables (`scannedTables`), the
952
+ * aggregate `scans` counter on the accumulator row advances by the distinct
953
+ * table count and one extra `(path, table)` upsert fires per scanned table.
954
+ * Indexed dispatches (the common case) skip all of that and pay nothing.
955
+ */
956
+ declare const recordFunctionMetric: (sql: SqlExec, input: RecordFunctionMetricInput) => void;
957
+ /**
958
+ * Read the per-function full-scan attribution, grouped by function path. The
959
+ * returned map keys are `path`; each value is the function's full-scanned
960
+ * tables ordered by scan count (busiest scan first), so the causal "slow
961
+ * BECAUSE it scanned X" read can lead with the dominant table. Creates the
962
+ * table first so reads on a never-called shard return an empty map.
963
+ */
964
+ declare const readFunctionMetricScans: (sql: SqlExec) => Map<string, FunctionScanAttribution[]>;
965
+ /**
966
+ * Read the per-`(table, index)` hit counts — the advisor dead-index lint input.
967
+ * Each entry is a declared index and how many recorded reads used it to narrow
968
+ * (a cumulative, non-decaying count); the lint reconciles this against the schema
969
+ * to flag a declared index that appears with zero reads (or not at all) as dead. Ordered
970
+ * by table then index for stable output. Creates the table first so a read on a
971
+ * never-exercised shard returns `[]`.
972
+ */
973
+ declare const readFunctionMetricIndexHits: (sql: SqlExec) => FunctionMetricIndexHit[];
974
+ /**
975
+ * Fold a dispatch's distinct full-scanned tables into an in-memory attribution
976
+ * list, mirroring the per-`(path, table)` upsert {@link recordFunctionMetric}
977
+ * applies to the durable `__lunora_metrics_scans` table. Kept here, beside its
978
+ * SQL twin, so the one rule (one occurrence = +1 scan for that table, list
979
+ * re-sorted busiest-first) lives in a single module — the in-memory copy exists
980
+ * only for the warm-instance fallback when the durable read is unavailable.
981
+ * Mutates and returns `into`.
982
+ */
983
+ declare const mergeScanAttribution: (into: FunctionScanAttribution[], scanned: ReadonlyArray<string>) => FunctionScanAttribution[];
984
+ /**
985
+ * Read the persisted per-function accumulators as {@link FunctionCallStat}s,
986
+ * newest-called first. Creates the table first so reads on a never-called shard
987
+ * return `[]` instead of throwing. The shape is a superset of the legacy
988
+ * in-memory `getFunctionStats` rows — the additive `scans` total and
989
+ * `scannedTables` causal attribution are folded in here so a single read backs
990
+ * the Insights "missing index" / "full scan" signal.
991
+ */
992
+ declare const readFunctionMetrics: (sql: SqlExec) => FunctionCallStat[];
993
+ /**
994
+ * Read the coarse time-series buckets for `path` (every path when omitted),
995
+ * oldest-bucket first so a chart can plot them left-to-right. Creates the table
996
+ * first so reads on a never-called shard return `{ buckets: [], truncated: false }`.
997
+ */
998
+ declare const readFunctionMetricBuckets: (sql: SqlExec, path?: string) => FunctionMetricBucketsResult;
999
+ /**
1000
+ * Aggregate the persisted accumulators into the lifetime totals the metrics
1001
+ * health snapshot reports: total calls (`requests`), total `errors`. Returns
1002
+ * zeroes on a never-called shard.
1003
+ *
1004
+ * `capped` is true when the distinct-path cap ({@link FUNCTION_METRICS_MAX_PATHS})
1005
+ * has been reached — the write-side signal that a brand-new function path is
1006
+ * currently being refused (see `admitPath`), mirroring `readQueryInsights`'s
1007
+ * `capped` for query-metrics and `readMetricHistory`'s for metric-history.
1008
+ */
1009
+ declare const readFunctionMetricsTotals: (sql: SqlExec) => {
1010
+ capped: boolean;
1011
+ errors: number;
1012
+ requests: number;
1013
+ };
1014
+ /**
1015
+ * The Workers AI text model the Issue explainer uses when the caller does not
1016
+ * override it. The fp8-fast instruct model the rest of the repo defaults to —
1017
+ * the explainer is a short, grounded rewrite (not a reasoning task), so a
1018
+ * latency-optimized build beats a larger one. Deliberately not the retired
1019
+ * `@cf/meta/llama-3.1-8b-instruct`: a deprecated model-id makes `binding.run`
1020
+ * throw, which would silently degrade every explain to `"ai-error"`.
1021
+ */
1022
+ /**
1023
+ * Fallback model id when neither the caller nor the request names one.
1024
+ *
1025
+ * A Workers AI id, which makes it the *host's* default rather than this
1026
+ * package's — a second host runs different models and must be able to say so.
1027
+ * `explainIssue` therefore takes `defaultModel`, and this constant is only what
1028
+ * the Cloudflare host happens to pass. Kept exported so that host has a name to
1029
+ * pass rather than a string literal.
1030
+ */
1031
+ declare const DEFAULT_EXPLAIN_ISSUE_MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
1032
+ /**
1033
+ * Structural projection of the Workers `AI` binding's `run` method — declared
1034
+ * locally so `@lunora/do` needs no dependency edge on `@lunora/ai` (nor on
1035
+ * `@cloudflare/workers-types`) to reach `env.AI`. Mirrors `AiBindingLike` in
1036
+ * `@lunora/ai`.
1037
+ */
1038
+ interface AiRunBinding {
1039
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
1040
+ }
1041
+ /** Parsed `__lunora_admin__:explainIssue` payload: the folded Issue's identifying facts, plus an optional model override. */
1042
+ interface ExplainIssueArgs {
1043
+ /** The Issue's culprit (`<file>:<function>` or `container:<name>`), for grounding context. */
1044
+ culprit?: string;
1045
+ /** Per-request model-id override; falls back to the caller's `defaultModel`, then {@link DEFAULT_EXPLAIN_ISSUE_MODEL}. */
1046
+ model?: string;
1047
+ /** A representative raw error message for the Issue — the fact the explanation is grounded in. Required. */
1048
+ sampleMessage: string;
1049
+ /** The Issue's human-readable title (first line of the sample message), for grounding context. */
1050
+ title?: string;
1051
+ }
1052
+ /**
1053
+ * Why the explainer fell back to the grounded hint alone. A closed union rather
1054
+ * than a bare `string` so `degraded(reason)` rejects a typo'd sentinel at compile
1055
+ * time instead of letting it fall through to the client's generic error copy.
1056
+ * Mirrored by `ExplainIssueResult["reason"]` in `@lunora/studio`'s `lib/admin.ts`.
1057
+ */
1058
+ type ExplainIssueDegradedReason = "ai-error" | "empty-response" | "no-ai-binding";
1059
+ /**
1060
+ * The grounding facts both `__lunora_admin__:explainIssue` outcomes carry. Present
1061
+ * whenever {@link findIssueSolution} recognized the message — offline,
1062
+ * deterministic, and independent of whether the AI path ran at all.
1063
+ *
1064
+ * The hint BODY is deliberately not on the wire: the client derives it from the
1065
+ * same catalog offline (that is the whole point of the grounded layer), so
1066
+ * shipping it would be payload nothing reads.
1067
+ */
1068
+ interface ExplainIssueGrounding {
1069
+ /**
1070
+ * The id of the matched catalog/platform solution the prompt was grounded in,
1071
+ * absent when nothing recognized the message. The client renders a caveat on
1072
+ * absence — an ungrounded explanation is a free-form model guess, not a
1073
+ * catalog-backed one, and must not be presented as the latter.
1074
+ */
1075
+ groundedId?: string;
1076
+ }
1077
+ /**
1078
+ * The `__lunora_admin__:explainIssue` result — a discriminated union on `degraded`
1079
+ * rather than a bag of optionals, so each outcome's guaranteed fields are
1080
+ * guaranteed in the type too. The AI `explanation` is best-effort: the degraded
1081
+ * arm is returned when no `env.AI` binding is configured or the inference call
1082
+ * failed, and the client falls back to its own grounded hint alone.
1083
+ *
1084
+ * Modelling this as one flat interface let a `degraded` result type-check without a
1085
+ * `reason`, which the studio silently renders as the generic AI-error copy.
1086
+ */
1087
+ /** The arm returned when no inference happened, or it failed. */
1088
+ interface ExplainIssueDegraded extends ExplainIssueGrounding {
1089
+ /** The AI path was unavailable or failed — render the grounded hint instead. */
1090
+ degraded: true;
1091
+ /** Why the AI path degraded, for the client to surface. */
1092
+ reason: ExplainIssueDegradedReason;
1093
+ }
1094
+ /** The arm returned when the model ran and produced text. */
1095
+ interface ExplainIssueSuccess extends ExplainIssueGrounding {
1096
+ /** The AI path ran and produced text. */
1097
+ degraded: false;
1098
+ /** The AI-generated plain-language explanation. */
1099
+ explanation: string;
1100
+ /** The Workers AI model-id that produced {@link ExplainIssueSuccess.explanation}. */
1101
+ model: string;
1102
+ }
1103
+ type ExplainIssueResult = ExplainIssueDegraded | ExplainIssueSuccess;
1104
+ /**
1105
+ * Validate the `__lunora_admin__:explainIssue` payload. Requires a non-empty
1106
+ * `sampleMessage` (the grounding fact); `title`, `culprit`, and `model` are
1107
+ * optional context/overrides. Throws a 400 `LunoraError` on a bad shape.
1108
+ *
1109
+ * Every caller-supplied field that reaches the prompt is capped here — capping
1110
+ * `sampleMessage` alone left `title`/`culprit` as an open door onto the same
1111
+ * prompt budget.
1112
+ */
1113
+ declare const parseExplainIssueArgs: (args: Record<string, unknown>) => ExplainIssueArgs;
1114
+ /**
1115
+ * Run the full explain flow for one Issue: validate the payload, ground it in the
1116
+ * catalog, and — when `binding` is a usable Workers AI binding — ask the model for
1117
+ * a plain-language rewrite. Never throws for an AI-side failure; every such path
1118
+ * returns the `degraded: true` arm carrying the grounded hint, so the caller
1119
+ * always has something to render. Only a malformed payload throws (a 400).
1120
+ *
1121
+ * `binding` is `unknown` so the caller can hand over `env.AI` untyped — the shape
1122
+ * check lives here rather than at each call site.
1123
+ */
1124
+ declare const explainIssue: (binding: unknown, args: Record<string, unknown>, options?: {
1125
+ defaultModel?: string;
1126
+ }) => Promise<ExplainIssueResult>;
1127
+ /** Reserved table holding one triage-state row per Issue fingerprint. Auto-hidden by the `__lunora` prefix. */
1128
+ declare const ISSUE_STATE_TABLE = "__lunora_issue_state__";
1129
+ /**
1130
+ * Triage status of an Issue. `open` is the implicit default (no state row); a
1131
+ * developer moves it to `resolved` (fixed, but a *new* matching error re-opens
1132
+ * it — see {@link readIssueStates}'s consumer) or `ignored` (deliberately muted,
1133
+ * and stays muted regardless of new occurrences).
1134
+ */
1135
+ type IssueStatus = "ignored" | "open" | "resolved";
1136
+ /** Ordered severity a developer can tag an Issue with; drives the Studio badge palette. */
1137
+ type IssueSeverity = "critical" | "high" | "low" | "medium";
1138
+ /** The persisted triage state for one Issue fingerprint. */
1139
+ interface IssueState {
1140
+ /** Free-form assignee (a userId or a name); absent when unassigned. */
1141
+ assignee?: string;
1142
+ /** Stable 16-char fingerprint hash — the same key `readErrorIssues` folds on. */
1143
+ hash: string;
1144
+ /** Developer-tagged severity; absent when untriaged. */
1145
+ severity?: IssueSeverity;
1146
+ /** Current triage status. */
1147
+ status: IssueStatus;
1148
+ /** Wall-clock millis the state was last changed — compared against an Issue's `lastSeen` to detect a regression. */
1149
+ updatedAt: number;
1150
+ /** Acting userId that last changed the state, when known. */
1151
+ updatedBy?: string;
1152
+ }
1153
+ /** Patch applied by an admin write; every field is optional so a caller can change one facet at a time. */
1154
+ interface IssueStatePatch {
1155
+ assignee?: null | string;
1156
+ severity?: IssueSeverity | null;
1157
+ status?: IssueStatus;
1158
+ }
1159
+ /** The valid {@link IssueStatus} values, for arg validation at the admin boundary. */
1160
+ declare const ISSUE_STATUSES: ReadonlyArray<IssueStatus>;
1161
+ /** The valid {@link IssueSeverity} values, for arg validation at the admin boundary. */
1162
+ declare const ISSUE_SEVERITIES: ReadonlyArray<IssueSeverity>;
1163
+ /**
1164
+ * Apply a triage patch to one Issue, upserting its state row. A `null` in the
1165
+ * patch clears the field (unassign, untag severity); an omitted field is left
1166
+ * unchanged. Returns the resulting {@link IssueState} so the caller can echo it.
1167
+ *
1168
+ * Uses `ON CONFLICT(hash) DO UPDATE` with `COALESCE(?, column)` per optional
1169
+ * field so a partial patch touches only what it names — except the explicit
1170
+ * `null` sentinels for assignee/severity, which are threaded separately so a
1171
+ * clear can win over `COALESCE`.
1172
+ */
1173
+ declare const upsertIssueState: (sql: SqlExec, hash: string, patch: IssueStatePatch, updatedAt: number, updatedBy?: string) => IssueState;
1174
+ /**
1175
+ * Severity of a buffered log entry — the full seven-tier `ctx.log` ramp
1176
+ * (`trace`→`fatal`), not a console-shaped subset. The buffer used to fold the
1177
+ * ramp onto four tiers, which made `trace` and `fatal` lines indistinguishable
1178
+ * from `debug` and `error` in the Studio Logs panel; it now stores the level the
1179
+ * caller actually logged at. Container-lifecycle entries only ever use
1180
+ * `info`/`error`, which remain part of the union.
1181
+ */
1182
+ type LogLevel = ContextLogLevel;
1183
+ /**
1184
+ * One buffered log line. `functionPath` is the RPC that produced it (when the
1185
+ * entry came from the RPC dispatch site); `timestamp` is `Date.now()` at the
1186
+ * moment it was pushed. `instance`/`exitCode` are populated for container
1187
+ * lifecycle entries: `instance` correlates the per-instance Durable Object id,
1188
+ * `exitCode` carries the process exit code parsed out of a `stop` event.
1189
+ */
1190
+ interface LogEntry {
1191
+ exitCode?: number;
1192
+ /** Structured fields from a `ctx.log.<level>(message, fields)` / `ctx.log.with(fields)` call, when present. */
1193
+ fields?: Record<string, unknown>;
1194
+ functionPath?: string;
1195
+ instance?: string;
1196
+ level: LogLevel;
1197
+ message: string;
1198
+ timestamp: number;
1199
+ }
1200
+ /**
1201
+ * A bounded, in-memory ring buffer of recent {@link LogEntry} records.
1202
+ *
1203
+ * In-memory only: like the metrics counters on `ShardDO`, the buffer is a field
1204
+ * on the live Durable Object instance and so resets whenever the DO hibernates
1205
+ * or restarts. It is a "recent activity on this instance" readout (for the
1206
+ * studio's live log panel), NOT a durable log store or a transport.
1207
+ * Production log shipping is the platform's job, not ours: use Cloudflare
1208
+ * **Workers Logs** (retained, queryable in the studio), **Logpush** (stream
1209
+ * to R2 / a SIEM / a log service), or a **Tail Worker** for programmatic
1210
+ * capture. Lunora deliberately does not reimplement any of those — this buffer
1211
+ * stays a tiny dev/ops readout. Capacity is fixed at construction; once full,
1212
+ * the oldest entry is evicted to make room (FIFO), so memory stays bounded
1213
+ * regardless of traffic.
1214
+ */
1215
+ declare class LogBuffer {
1216
+ /** Backing store, kept in insertion order (oldest first). */
1217
+ private readonly buffer;
1218
+ private readonly capacity;
1219
+ constructor(capacity?: number);
1220
+ /** Number of entries currently buffered. */
1221
+ get size(): number;
1222
+ /** Drop every buffered entry. */
1223
+ clear(): void;
1224
+ /**
1225
+ * Snapshot of the buffered entries, **newest first** so the panel renders
1226
+ * the most recent activity at the top without re-sorting. Returns a fresh
1227
+ * array each call; the caller may mutate it freely.
1228
+ */
1229
+ entries(): LogEntry[];
1230
+ /**
1231
+ * Append an entry, evicting the oldest when at capacity so the buffer never
1232
+ * grows past its bound.
1233
+ */
1234
+ push(entry: LogEntry): void;
1235
+ }
1236
+ /**
1237
+ * One aggregated metric series: every measurement sharing a `(name, kind,
1238
+ * attributes)` identity folded into a single running summary.
1239
+ *
1240
+ * All fields are maintained for every {@link MetricKind} — the fold is uniform
1241
+ * and O(1), and letting the panel choose the meaningful projection per kind
1242
+ * (counter → `sum`, gauge → `last`, histogram → `sum`/`count` for the mean, plus
1243
+ * `min`/`max`) is cheaper and clearer than branching on kind at record time.
1244
+ */
1245
+ interface MetricSeries {
1246
+ /** The series' dimensions, if any — the attributes that made it distinct. */
1247
+ attributes?: LogFields;
1248
+ /** Number of measurements folded into this series. */
1249
+ count: number;
1250
+ /** Trace id of the most recent measurement that carried one — the series' exemplar, for linking to a trace. */
1251
+ exemplarTraceId?: string;
1252
+ /** Wall-clock millis of the first measurement folded in. */
1253
+ firstTs: number;
1254
+ /** Function path that recorded the series' most recent measurement. */
1255
+ functionPath: string;
1256
+ /** Instrument kind; decides which projection the panel shows. */
1257
+ kind: MetricKind;
1258
+ /** Most recent measured value — the current reading for a `gauge`. */
1259
+ last: number;
1260
+ /** Wall-clock millis of the most recent measurement. */
1261
+ lastTs: number;
1262
+ /** Largest measured value seen. */
1263
+ max: number;
1264
+ /** Smallest measured value seen. */
1265
+ min: number;
1266
+ /** Instrument name, e.g. `"orders.placed"`. */
1267
+ name: string;
1268
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
1269
+ shardKey?: string;
1270
+ /** Sum of measured values — a `counter`'s total, a `histogram`'s sum. */
1271
+ sum: number;
1272
+ }
1273
+ /**
1274
+ * A bounded map of running metric aggregates, keyed by series identity. Eviction
1275
+ * is least-recently-*updated*: a re-recorded series moves back to the tail (Map
1276
+ * insertion order), so the capacity bound sheds cold, high-cardinality series and
1277
+ * keeps the ones still receiving traffic — the opposite of a raw ring, which
1278
+ * would evict a hot counter's own history.
1279
+ */
1280
+ declare class MetricBuffer {
1281
+ private readonly capacity;
1282
+ private readonly series;
1283
+ constructor(capacity?: number);
1284
+ /** Number of distinct series currently aggregated. */
1285
+ get size(): number;
1286
+ /** Drop every aggregated series. */
1287
+ clear(): void;
1288
+ /**
1289
+ * Snapshot of the aggregated series, most-recently-updated first, each a fresh
1290
+ * copy so a caller can't mutate the live aggregate. `series` is kept in
1291
+ * update order (tail = newest), so one reverse yields newest-first.
1292
+ */
1293
+ entries(): MetricSeries[];
1294
+ /** Fold one measurement into its series, creating or updating the aggregate. */
1295
+ push(event: MetricEvent): void;
1296
+ }
1297
+ /** One time-bucket sample of a series: the aggregate over `[bucketMs, bucketMs + METRIC_HISTORY_BUCKET_MS)`. */
1298
+ interface MetricHistoryPoint {
1299
+ /** Epoch-ms floor of the bucket window. */
1300
+ bucketMs: number;
1301
+ /** Measurements folded into this bucket. */
1302
+ count: number;
1303
+ /** Sample `traceId` of a measurement in this bucket, if one carried trace context — the exemplar. */
1304
+ exemplarTraceId?: string;
1305
+ /** Last measured value in the bucket — a gauge's reading at the window's end. */
1306
+ last: number;
1307
+ /** Largest value in the bucket. */
1308
+ max: number;
1309
+ /** Smallest value in the bucket. */
1310
+ min: number;
1311
+ /** Sum of values in the bucket — a counter's increment total, a histogram's sum. */
1312
+ sum: number;
1313
+ }
1314
+ /** One series' durable history: its identity plus its time-ordered buckets, oldest first. */
1315
+ interface MetricHistorySeries {
1316
+ attributes?: LogFields;
1317
+ functionPath: string;
1318
+ kind: MetricKind;
1319
+ name: string;
1320
+ /** Buckets in ascending `bucketMs` order, ready to chart as a line. */
1321
+ points: MetricHistoryPoint[];
1322
+ shardKey?: string;
1323
+ }
1324
+ /** {@link readMetricHistory} result: every tracked series with its buckets. */
1325
+ interface MetricHistoryResult {
1326
+ /**
1327
+ * True when the distinct-series cap has been reached — a **write-side**
1328
+ * signal ("this shard can no longer admit a brand-new series", see
1329
+ * `admitNewSeries`), not a read-side truncation flag. It is computed over
1330
+ * the whole table regardless of `options.sinceMs`/the row-count read
1331
+ * limit, so it can be `true` even when every series `readMetricHistory`
1332
+ * actually returned fits comfortably: the caller should read it as "a
1333
+ * flood of new series would currently be refused", the same thing
1334
+ * `readQueryInsights`'s `capped` already signals for query-metrics.
1335
+ */
1336
+ capped: boolean;
1337
+ series: MetricHistorySeries[];
1338
+ }
1339
+ /**
1340
+ * Tunable caps for {@link recordMetricHistory}, threaded from the sink's
1341
+ * `metricHistory` option. Each falls back to its module-constant default, so an
1342
+ * omitted field keeps the historical behaviour.
1343
+ */
1344
+ interface MetricHistoryOptions {
1345
+ /** Distinct series tracked before the least-recently-updated one is evicted to admit a new one (default {@link METRIC_HISTORY_MAX_SERIES}). */
1346
+ maxSeries?: number;
1347
+ /** Minute-buckets kept per series before older rows are trimmed (default {@link METRIC_HISTORY_BUCKET_RETENTION}). */
1348
+ retentionBuckets?: number;
1349
+ }
1350
+ /**
1351
+ * Fold one measurement into its `(series, minute)` bucket. Runs per
1352
+ * `ctx.metrics.*` call (unlike `function-metrics.ts`, which runs once per
1353
+ * dispatch), so it's tuned for the in-minute repeat: a bucket this instance has
1354
+ * already written is a single upsert (no reads), a bucket only in the DB costs one
1355
+ * PK point-lookup + upsert, and only a genuinely new bucket also pays the
1356
+ * distinct-series cap scan + retention trim. Still a durable SQLite write per
1357
+ * measurement, so a hot loop recording thousands of points a second should
1358
+ * pre-aggregate and record once (see `shared/metric-event.ts`). Creates the table
1359
+ * first so callers needn't.
1360
+ *
1361
+ * `exemplarTraceId` (the recording dispatch's trace, when it had one) is stored on
1362
+ * the bucket so the studio can link a chart point back to a trace. Latest wins:
1363
+ * a later sample carrying a trace replaces an earlier bucket's exemplar.
1364
+ *
1365
+ * `options` tunes the distinct-series cap and retention window from the sink's
1366
+ * `metricHistory` flag (see {@link MetricHistoryOptions}); each defaults to its
1367
+ * module constant.
1368
+ */
1369
+ declare const recordMetricHistory: (sql: SqlExec, event: MetricEvent, exemplarTraceId?: string, options?: MetricHistoryOptions) => void;
1370
+ /**
1371
+ * Read the durable history, grouped into one {@link MetricHistorySeries} per
1372
+ * series with its buckets in ascending time order.
1373
+ *
1374
+ * Rows are fetched most-recent-first (`bucket_ms DESC`) under the row cap, NOT
1375
+ * `series_key`-ordered: all active series write the same recent minutes, so this
1376
+ * windows every series to a recent slice fairly, instead of handing the
1377
+ * alphabetically-first series its full 1440-bucket history and starving the rest
1378
+ * once the cap is hit. Each series' points are re-sorted ascending below, since a
1379
+ * trend line reads oldest→newest.
1380
+ *
1381
+ * `options.sinceMs`, when set, returns only buckets at or after this epoch-ms —
1382
+ * the studio's time-window selector. `options.maxSeries`, mirroring the write
1383
+ * side's tunable, is only used to compute `capped` — it does not affect which
1384
+ * rows are read.
1385
+ */
1386
+ declare const readMetricHistory: (sql: SqlExec, options?: {
1387
+ maxSeries?: number;
1388
+ sinceMs?: number;
1389
+ }) => MetricHistoryResult;
1390
+ /** One row of the `__lunora_metrics_queries` table, as returned by `readQueryMetrics`. */
1391
+ interface QueryStatEntry {
1392
+ /** Total number of times this statement was executed. */
1393
+ execCount: number;
1394
+ /** Normalised SQL text (literals stripped, truncated). */
1395
+ normalizedSql: string;
1396
+ /** Total rows read across all executions (SELECT result sizes). */
1397
+ rowsRead: number;
1398
+ /** Total rows written across all executions. */
1399
+ rowsWritten: number;
1400
+ /** Total wall-clock milliseconds across all executions. */
1401
+ totalDurationMs: number;
1402
+ }
1403
+ /** One statement's activity within a chosen time range. */
1404
+ interface QueryInsightEntry {
1405
+ /** Mean milliseconds per execution across the range. */
1406
+ avgDurationMs: number;
1407
+ execCount: number;
1408
+ normalizedSql: string;
1409
+ /** Interpolated 50th percentile latency, in milliseconds. */
1410
+ p50DurationMs: number;
1411
+ /** Interpolated 95th percentile latency, in milliseconds. */
1412
+ p95DurationMs: number;
1413
+ rowsRead: number;
1414
+ rowsWritten: number;
1415
+ totalDurationMs: number;
1416
+ }
1417
+ /** One point on the throughput/latency charts. */
1418
+ interface QueryInsightBucket {
1419
+ /** Mean milliseconds per execution in this window, across all statements. */
1420
+ avgDurationMs: number;
1421
+ /** Bucket start, epoch millis. */
1422
+ bucketMs: number;
1423
+ execCount: number;
1424
+ }
1425
+ /** What `getQueryInsights` returns. */
1426
+ interface QueryInsightsResult {
1427
+ /** Time series across the whole range, all statements combined. */
1428
+ buckets: QueryInsightBucket[];
1429
+ /**
1430
+ * True when the tracked-statement cap has been reached, so the caller can say
1431
+ * "showing N of a capped set" rather than implying totality. Silent
1432
+ * truncation reads as complete coverage when it is not.
1433
+ */
1434
+ capped: boolean;
1435
+ entries: QueryInsightEntry[];
1436
+ /** How many distinct statements the lifetime table is tracking. */
1437
+ trackedStatements: number;
1438
+ }
1439
+ /**
1440
+ * Per-statement activity within `rangeMs` of `now`, plus a combined time series.
1441
+ *
1442
+ * Reads the bucket table (not the lifetime one) so the numbers answer "what is
1443
+ * hot right now"; the statement TEXT is joined back from the lifetime table,
1444
+ * which is the only place it is stored.
1445
+ */
1446
+ declare const readQueryInsights: (sql: SqlExec, rangeMs: number, now?: number) => QueryInsightsResult;
1447
+ /**
1448
+ * Record one statement's activity. Creates the table on first call. Silently
1449
+ * skips recording when the normalised statement is empty (shouldn't happen
1450
+ * in practice) or when the table is already at the
1451
+ * {@link QUERY_METRICS_MAX_STATEMENTS} cap and the statement is not yet
1452
+ * tracked. See `admitStatement` for how the cap check avoids an unconditional
1453
+ * `COUNT(*)` on every execution.
1454
+ *
1455
+ * `execCount` (default 1) lets a caller fold several executions of the SAME
1456
+ * statement into one call — `shard-do.ts` does this per dispatch so a
1457
+ * query-in-a-loop handler pays one upsert here instead of one per raw
1458
+ * execution. `durationMs`/`rowsRead`/`rowsWritten` are then the SUM across
1459
+ * `execCount` executions, exactly as `exec_count`/`total_duration_ms` already
1460
+ * accumulate sums across separate calls — folding before calling is
1461
+ * indistinguishable, from this table's point of view, from `execCount`
1462
+ * separate calls with the same totals.
1463
+ */
1464
+ declare const recordQueryMetric: (sql: SqlExec, rawSql: string, durationMs: number, rowsRead: number, rowsWritten: number, now?: number, execCount?: number) => void;
1465
+ /**
1466
+ * Read all tracked statement aggregates, ordered by `total_duration_ms DESC`
1467
+ * (the leaderboard's default). Creates the table first so a read on a
1468
+ * never-called shard returns `[]`.
1469
+ */
1470
+ declare const readQueryMetrics: (sql: SqlExec) => QueryStatEntry[];
1471
+ /** Reserved append-only table backing the studio Logs tab. Auto-hidden from the data browser by the `__lunora` prefix. */
1472
+ declare const REQUEST_LOG_TABLE = "__lunora_reqlog__";
1473
+ /** Outcome of one dispatch — `ok` for a returned result, `error` for a thrown handler. */
1474
+ type RequestOutcome = "error" | "ok";
1475
+ /** One recorded `/rpc` dispatch, in monotonic `seq` order. */
1476
+ interface RequestLogEntry {
1477
+ /** Whether the result was served from the reactive cache; `undefined` when the cache is disabled or the path isn't cached (a write/action). */
1478
+ cacheHit?: boolean;
1479
+ /** Handler wall-clock duration in milliseconds (before the subscription write-flush, matching the per-function metrics). */
1480
+ durationMs: number;
1481
+ /** Error message when `outcome === "error"`, redacted like args/identity; absent on success. */
1482
+ errorMessage?: string;
1483
+ /** The `<file>:<function>` identifier dispatched, e.g. `messages:list`. */
1484
+ functionPath: string;
1485
+ /** Identity-claim envelope forwarded by the runtime, JSON-decoded; leaf values are redacted (the claims are PII), so only the shape survives. Absent for anonymous requests. Correlate on `userId` instead. */
1486
+ identity?: Record<string, unknown>;
1487
+ /** `ok` for a returned result, `error` for a thrown handler. */
1488
+ outcome: RequestOutcome;
1489
+ /** Call args with leaf values redacted by default (keys/shape preserved); absent when no args were sent. */
1490
+ redactedArgs?: unknown;
1491
+ /** Monotonic per-shard cursor — strictly increasing, never reused. */
1492
+ seq: number;
1493
+ /** Shard key (the DO id name), or `undefined` for the unnamed `__root__` DO. */
1494
+ shardKey?: string;
1495
+ /** Count of subscriptions re-run by the write this dispatch triggered; `0` when none (or not measured at the dispatch site). */
1496
+ subscriptionsReRun: number;
1497
+ /** Tables the handler read (from the dependency tracker); empty when the reactive cache is off or the path read nothing. */
1498
+ tablesRead: string[];
1499
+ /** Tables the handler wrote (from the change tracker); empty for a read-only dispatch. */
1500
+ tablesWritten: string[];
1501
+ /** Wall-clock millis when the dispatch completed. */
1502
+ ts: number;
1503
+ /** Acting userId forwarded by the runtime, or `undefined` when anonymous. */
1504
+ userId?: string;
1505
+ }
1506
+ /** Fields accepted when appending one request-log entry; `seq` is assigned by the table. */
1507
+ interface AppendRequestLogEntry {
1508
+ cacheHit?: boolean;
1509
+ durationMs: number;
1510
+ errorMessage?: string;
1511
+ functionPath: string;
1512
+ identity?: Record<string, unknown>;
1513
+ outcome: RequestOutcome;
1514
+ redactedArgs?: unknown;
1515
+ shardKey?: string;
1516
+ subscriptionsReRun?: number;
1517
+ tablesRead?: string[];
1518
+ tablesWritten?: string[];
1519
+ ts: number;
1520
+ userId?: string;
1521
+ }
1522
+ /** Knobs the dispatch site threads into a request-log write. */
1523
+ interface RequestLogWriteOptions {
1524
+ /** When `true` (development only), skip args/identity redaction so a developer sees raw values. Defaults to `false` (production-safe). */
1525
+ captureRaw?: boolean;
1526
+ /** Rows to keep after the append-time trim; defaults to {@link REQUEST_LOG_RETENTION}. The operator's `LUNORA_REQUEST_LOG_RETENTION` override. */
1527
+ retention?: number;
1528
+ }
1529
+ /** Filters for {@link readRequestLog}, all AND-combined; every value is a bound SQL parameter, so nothing here injects SQL. */
1530
+ interface ReadRequestLogOptions {
1531
+ /** Functions whose path begins with this prefix (a `<file>:` or `<file>:<fn>` correlation). */
1532
+ functionPathPrefix?: string;
1533
+ /** Upper bound on returned rows, clamped to [1, 10000]. */
1534
+ limit?: number;
1535
+ /** Keep only `ok` / `error` outcomes. */
1536
+ outcome?: RequestOutcome;
1537
+ /** Exact shard-key match. */
1538
+ shardKey?: string;
1539
+ /** Only entries strictly after this cursor (forward paging). */
1540
+ sinceSeq?: number;
1541
+ /** Keep only entries whose read OR written table set contains this table. */
1542
+ tableTouched?: string;
1543
+ /** Exact acting-userId match. */
1544
+ userId?: string;
1545
+ }
1546
+ /** Payload of a `__lunora_admin__:getRequestLog` call: the recorded entries, newest first. */
1547
+ interface RequestLogResult {
1548
+ entries: RequestLogEntry[];
1549
+ }
1550
+ /**
1551
+ * One grouped error **Issue**: many `error`-outcome request-log rows that share a
1552
+ * fingerprint folded into a single triage row. The `hash` is the same stable key
1553
+ * a cloud Incident groups on, so a local Issue and a cloud Incident are the same
1554
+ * object.
1555
+ */
1556
+ interface ErrorIssue {
1557
+ /** Assignee (a userId or a name) from the persisted triage state; absent when unassigned. */
1558
+ assignee?: string;
1559
+ /** Number of `error` rows folded into this Issue within the scanned window. */
1560
+ count: number;
1561
+ /** The `<file>:<function>` (or `container:<name>`) the errors came from. */
1562
+ culprit: string;
1563
+ /** Wall-clock millis of the oldest folded row. */
1564
+ firstSeen: number;
1565
+ /**
1566
+ * Stable 16-char grouping hash over `functionPath :: bucket(message)`,
1567
+ * computed from the RAW (pre-redaction) message at write time and stored on
1568
+ * the row — see {@link appendRequestLogEntry} — so redacting `sampleMessage`
1569
+ * below can't change the grouping.
1570
+ */
1571
+ hash: string;
1572
+ /** Wall-clock millis of the newest folded row. */
1573
+ lastSeen: number;
1574
+ /** A representative error message (redacted, like the durable row) — taken from the most recent folded row. */
1575
+ sampleMessage: string;
1576
+ /** Developer-tagged severity from the persisted triage state; absent when untriaged. */
1577
+ severity?: IssueSeverity;
1578
+ /**
1579
+ * Wall-clock millis the persisted triage state was last changed; absent when
1580
+ * the Issue has never been triaged. Compared against `lastSeen` to detect a
1581
+ * regression (a new error after a resolve).
1582
+ */
1583
+ stateUpdatedAt?: number;
1584
+ /**
1585
+ * Triage status folded in from the persisted state (`open` by default). A
1586
+ * `resolved` Issue whose `lastSeen` is newer than `stateUpdatedAt` is
1587
+ * auto-reopened to `open` here (a regression), so a fresh occurrence never
1588
+ * hides behind a stale resolution; `ignored` stays sticky by design.
1589
+ */
1590
+ status: IssueStatus;
1591
+ /** Human-readable title (first line of the sample message, capped). */
1592
+ title: string;
1593
+ }
1594
+ /** Payload of a `__lunora_admin__:getIssues` call: grouped error Issues, most-recently-active first. */
1595
+ interface IssuesResult {
1596
+ issues: ErrorIssue[];
1597
+ }
1598
+ /** Filters for {@link readErrorIssues}; forwarded to {@link readRequestLog} with `outcome` forced to `error`. */
1599
+ interface ReadIssuesOptions {
1600
+ /** Functions whose path begins with this prefix (a `<file>:` or `<file>:<fn>` correlation). */
1601
+ functionPathPrefix?: string;
1602
+ /** Upper bound on error rows scanned before grouping, clamped to [1, 10000]. */
1603
+ limit?: number;
1604
+ /** Exact shard-key match. */
1605
+ shardKey?: string;
1606
+ /** Keep only Issues in this triage status, applied AFTER the persisted-state fold + auto-reopen. */
1607
+ status?: IssueStatus;
1608
+ /** Exact acting-userId match. */
1609
+ userId?: string;
1610
+ }
1611
+ /**
1612
+ * Redact the secrets / PII out of a value before it reaches the durable log or a
1613
+ * Logpush event, via `@visulima/redact`'s `standardRules`. Unlike a blunt
1614
+ * type-tag stamp this masks sensitive values by PATTERN (not just by key name)
1615
+ * while leaving benign values readable, so the studio's args/identity columns
1616
+ * stay useful. `null` / `undefined` pass through unchanged.
1617
+ *
1618
+ * What `standardRules` actually catches differs by shape, verified against its
1619
+ * real behavior rather than assumed from its name: on a KEYED object (`args`,
1620
+ * `identity`) it also matches by key name, so `{ password: "hunter2" }` and
1621
+ * `{ token: "…" }` ARE masked regardless of the value's shape. On a PLAIN
1622
+ * STRING — which is what `errorMessage`/log `fields`-as-rendered-text are —
1623
+ * only pattern-shaped matches apply: emails, long digit runs / structured
1624
+ * numeric IDs (credit-card, phone, SSN, AWS-access-key-style), and an explicit
1625
+ * `Bearer <token>` / `token=…`-shaped substring. A free-text `password=hunter2`
1626
+ * or a bare provider API key embedded in prose (e.g. `sk-live-…`) is NOT
1627
+ * caught on a plain string — there is no key to match against, and neither is
1628
+ * a recognized value pattern. So this is a PII-pattern net for rendered text,
1629
+ * not a general secrets scrubber; a handler that echoes a raw credential into
1630
+ * an error message or a log string can still leak it through here. Works on a
1631
+ * plain string too (`redact` traverses whatever value it's handed), which is
1632
+ * how {@link appendRequestLogEntry} and {@link emitRequestLogEvent} reuse this
1633
+ * for `errorMessage` — a validation error echoes the offending value, a
1634
+ * constraint error quotes the conflicting row, so the error message is at
1635
+ * least as PII-dense as args and gets the same treatment (with the free-text
1636
+ * caveat above).
1637
+ *
1638
+ * `captureRaw` is the development escape hatch: in a dev environment the dispatch
1639
+ * site (`isDevEnvironment`) passes `true` to skip redaction so a developer can
1640
+ * see real arg/identity/error values; production always redacts. The dev
1641
+ * decision is made at the call site from the deployment env, never inferred
1642
+ * here — so a real deploy that omits the env var stays redacted.
1643
+ */
1644
+ declare const redactArgs: (value: unknown, captureRaw?: boolean) => unknown;
1645
+ /**
1646
+ * Create the `__lunora_reqlog__` table. `seq` is an `AUTOINCREMENT` primary
1647
+ * key, giving each shard a monotonic cursor the Logs tab pages through; the
1648
+ * `args`/`identity`/`tables_read`/`tables_written` columns hold JSON and are
1649
+ * `NULL`/empty when none was recorded. Idempotent, so read and write paths can
1650
+ * call it defensively.
1651
+ *
1652
+ * `error_fingerprint` is the {@link fingerprintError} grouping hash captured
1653
+ * from the RAW `error_message` at write time, before {@link appendRequestLogEntry}
1654
+ * redacts it — see that function's docstring. It is added via a guarded
1655
+ * `ALTER TABLE` rather than baked into the `CREATE`, mirroring
1656
+ * `function-metrics.ts`'s `ensureFunctionMetricsTables`, so a shard whose
1657
+ * `__lunora_reqlog__` predates this column gains it on the next call without a
1658
+ * migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
1659
+ * error from a re-run (or the freshly-created schema above) is swallowed.
1660
+ */
1661
+ declare const ensureRequestLogTable: (sql: SqlExec) => void;
1662
+ /**
1663
+ * Append one dispatch to the request log, then trim the log back to the most
1664
+ * recent `retention` rows (default {@link REQUEST_LOG_RETENTION}). Creates the
1665
+ * table first so callers needn't. Args/identity/error message are redacted here
1666
+ * so a raw value never reaches the durable table — callers pass the unredacted
1667
+ * entry and rely on this, unless `captureRaw` (dev only) is set. `retention` is
1668
+ * the operator's `LUNORA_REQUEST_LOG_RETENTION` override, threaded in by the
1669
+ * dispatch site.
1670
+ *
1671
+ * The error-grouping fingerprint is computed from `entry.errorMessage` BEFORE
1672
+ * it's redacted below, and the resulting hash is persisted in
1673
+ * `error_fingerprint`. `readErrorIssues` groups off that stored hash instead of
1674
+ * recomputing `fingerprintError` from the (redacted) `error_message` column, so
1675
+ * masking a PII-bearing value — e.g. two different `<n>`-bucketed IDs that
1676
+ * redact to two different tag lengths (`<DL>` vs `<BANKACC>`) — can't split an
1677
+ * existing Issue or change its identity.
1678
+ */
1679
+ declare const appendRequestLogEntry: (sql: SqlExec, entry: AppendRequestLogEntry, options?: RequestLogWriteOptions) => void;
1680
+ /**
1681
+ * Emit one structured request event to `console` so Cloudflare's Workers Logs /
1682
+ * Logpush pipeline carries it to external sinks (SIEMs) — PLAN3 §3.3. This does
1683
+ * NOT reimplement a transport: it produces a richer, lunora-attributed event and
1684
+ * lets CF's existing trace-log pipe ship it. The event mirrors the durable
1685
+ * `__lunora_reqlog__` row (function path, shard, user, outcome, duration, tables
1686
+ * read/written, cache hit), with `args`, `identity`, AND `error` redacted
1687
+ * exactly like the durable write so no raw PII/secret reaches the log pipeline.
1688
+ *
1689
+ * An `error` outcome goes to `console.error` (surfacing at error level in the
1690
+ * trace so a SIEM can alert on it); everything else to `console.log`. The
1691
+ * `source: "lunora"` / `type: "request"` envelope lets a consumer filter these
1692
+ * events out of the raw Workers-trace firehose. `captureRaw` (dev only) skips
1693
+ * redaction, mirroring the durable write. Best-effort by contract — the caller
1694
+ * wraps it so a serialization hiccup can never fail the served request.
1695
+ */
1696
+ declare const emitRequestLogEvent: (entry: AppendRequestLogEntry, options?: RequestLogWriteOptions) => void;
1697
+ /**
1698
+ * The `ctx.log` event contract (shape + severity union) lives in
1699
+ * `shared/log-event.ts` (inlined into each `dist`) so the DO that builds these
1700
+ * events and the `@lunora/runtime` sink that consumes them agree by construction.
1701
+ * `LogEventInput` is the DO's historical name for the shared `LogEvent`.
1702
+ */
1703
+ type LogEventInput = LogEvent;
1704
+ /**
1705
+ * Split a `ctx.log.<level>(...)` call's raw arguments into a display `message`
1706
+ * and optional structured `fields`. The structured form — a message string plus
1707
+ * a plain-object fields bag — is matched only for exactly `(string, object)`;
1708
+ * every other shape is console-style and rendered whole (so existing
1709
+ * `console`-shaped calls are unchanged). Bound `.with(...)` fields merge under
1710
+ * the per-call fields (per-call wins); the result is normalized to a fresh bag
1711
+ * of JSON-safe primitives, or `undefined` when empty (see `normalizeLogFields`).
1712
+ */
1713
+ declare const parseLogArgs: (args: unknown[], boundFields?: LogFields) => {
1714
+ fields?: LogFields;
1715
+ message: string;
1716
+ };
1717
+ /**
1718
+ * Emit one application-log event from a `ctx.log.*` call to `console`, tagged
1719
+ * `{ source: "lunora", type: "log" }` so the CLI / Vite formatter can pretty-print
1720
+ * it in the dev terminal and a Logpush/SIEM consumer can filter it out of the
1721
+ * raw Workers-trace firehose.
1722
+ *
1723
+ * Only the rendered `message` is emitted here, NOT the structured `args` array:
1724
+ * the console event rides CF Workers Logs / Logpush to prod, and shipping raw,
1725
+ * un-redacted arg objects on a `source: "lunora"` line a SIEM is told to trust
1726
+ * would be a surprising PII/secret surface. The raw `args` stay on the in-process
1727
+ * `sink.onLog` path (`recordUserLog`), which the operator opts into and controls.
1728
+ * `message` already carries the developer's rendered values, exactly like a raw
1729
+ * `console.log` line.
1730
+ *
1731
+ * `error`/`fatal` go to `console.error`, `warn` to `console.warn` (so they
1732
+ * surface at the right level in the trace); every other level to `console.log`.
1733
+ *
1734
+ * Structured `fields` (plus `traceId`/`spanId` for correlation) ARE emitted here
1735
+ * — they are intentional metadata a log pipeline filters on, unlike raw `args`.
1736
+ * Unlike `args`, `fields` IS redacted before it rides this console line — a
1737
+ * developer can attach anything to a fields bag (`ctx.log.info("charged",
1738
+ * { email, cardLast4 })`), and this is the one line that's told to a SIEM as
1739
+ * trustworthy, exactly like the request-log `args`/`identity`/`error` columns.
1740
+ * `options.captureRaw` (dev only) skips it, mirroring every other redaction
1741
+ * point in this module; the sole current caller (`ShardDO.recordUserLog`)
1742
+ * doesn't yet thread a dev flag through, so `fields` redacts unconditionally
1743
+ * there today — a conservative default, never a correctness gap. A field value
1744
+ * that can't be serialised (a circular object) would make `JSON.stringify`
1745
+ * throw and drop the whole line, so serialisation falls back to a fields-free
1746
+ * line rather than losing the event.
1747
+ */
1748
+ declare const emitLogEvent: (input: LogEventInput, options?: RequestLogWriteOptions) => void;
1749
+ /**
1750
+ * Read request-log entries newest-first, AND-combining the supplied filters
1751
+ * (function-path prefix, exact userId/shardKey/outcome, and a table-touched
1752
+ * match against the read OR written table sets), up to `limit` (clamped to
1753
+ * [1, 10000]). Each value is a bound parameter, so no filter can inject SQL.
1754
+ * Creates the table first so reads on a never-logged shard return `[]` instead
1755
+ * of throwing. Mirrors `readAuditLog`/`readCdcChanges`.
1756
+ */
1757
+ declare const readRequestLog: (sql: SqlExec, options?: ReadRequestLogOptions) => RequestLogEntry[];
1758
+ declare const readErrorIssues: (sql: SqlExec, options?: ReadIssuesOptions) => ErrorIssue[];
1759
+ /**
1760
+ * Ordering/visual weight of a security finding — mirrors the studio's insight
1761
+ * severities so the Security Advisor and the Performance Advisor (Insights) share
1762
+ * one badge vocabulary. `error` sorts worst-first, then `warning`, then `info`.
1763
+ */
1764
+ type SecurityFindingLevel = "error" | "info" | "warning";
1765
+ /**
1766
+ * Which security heuristic fired. The detection stays free of presentation
1767
+ * strings — the studio maps each kind to a localized title, explanation, and
1768
+ * remediation hint — so the rule set is trivially unit-testable and the wire
1769
+ * payload is tiny.
1770
+ *
1771
+ * `admin-token-weak`: `LUNORA_ADMIN_TOKEN` is set but short enough to be brute-forceable. (An *unset* token disables admin introspection entirely, so this audit — itself admin-gated — only ever runs with a token present.)
1772
+ *
1773
+ * `ws-gate-open`: admin HTTP RPCs require the bearer, but `LUNORA_WS_BEARER` is unset so the WebSocket upgrade gate defaults open — live admin subscriptions (Logs, Metrics, …) are reachable without a credential.
1774
+ *
1775
+ * `dev-args-unredacted`: the worker reports a development environment, so the durable request log captures raw, un-redacted args and identity (PII). A production deploy mislabeled as dev would persist sensitive payloads.
1776
+ *
1777
+ * `auth-secret-weak`: `AUTH_SECRET` / `BETTER_AUTH_SECRET` is set but shorter than 32 chars — too little entropy to sign session tokens safely. Pairs with `admin-token-weak`.
1778
+ *
1779
+ * `cors-wildcard-credentials`: `LUNORA_ALLOWED_ORIGINS` includes a `*` wildcard while `LUNORA_CORS_ALLOW_CREDENTIALS` is on — browsers reject the combination and it defeats the allowlist, so credentialed cross-origin requests are effectively unguarded.
1780
+ *
1781
+ * `security-headers-disabled`: the deployment set `LUNORA_SECURITY_HEADERS` off, so HSTS / CSP / nosniff / frame-options are not applied — a real exposure on a production worker.
1782
+ *
1783
+ * `csrf-disabled`: the deployment set `LUNORA_SECURITY_CSRF` off, so the cross-origin state-change guard is down — cookie-authenticated mutations are forgeable on a production worker.
1784
+ *
1785
+ * `cookies-insecure`: `BETTER_AUTH_URL` is a plaintext `http://` origin on a non-dev worker, so session cookies cannot carry the `Secure` attribute and ride in cleartext.
1786
+ */
1787
+ type SecurityFindingKind = "admin-token-weak" | "auth-secret-weak" | "cookies-insecure" | "cors-wildcard-credentials" | "csrf-disabled" | "dev-args-unredacted" | "security-headers-disabled" | "ws-gate-open";
1788
+ /**
1789
+ * One detected security issue. `detail` carries kind-specific context the studio
1790
+ * may interpolate into the localized copy (e.g. the offending token length);
1791
+ * absent when the kind needs none.
1792
+ */
1793
+ interface SecurityFinding {
1794
+ detail?: Record<string, unknown>;
1795
+ kind: SecurityFindingKind;
1796
+ level: SecurityFindingLevel;
1797
+ }
1798
+ /** Payload of a `__lunora_admin__:getSecurityAudit` call: every detected finding, worst-first. */
1799
+ interface SecurityAuditResult {
1800
+ findings: SecurityFinding[];
1801
+ }
1802
+ /**
1803
+ * Minimum `LUNORA_ADMIN_TOKEN` length considered safe against brute force. A
1804
+ * short token gates the studio's destructive admin ops (writeRow, clearTable,
1805
+ * pitrRestore, …), so a guessable one is a real exposure. 24 chars ≈ 128 bits
1806
+ * for a random base64-ish token.
1807
+ */
1808
+ declare const MIN_ADMIN_TOKEN_LENGTH = 24;
1809
+ /**
1810
+ * Minimum `AUTH_SECRET` / `BETTER_AUTH_SECRET` length. better-auth signs session
1811
+ * tokens with this secret; 32 chars (≈ `openssl rand -hex 32` → 32 bytes hex, or
1812
+ * 192 bits of base64) is the floor below which the signing key is brute-forceable.
1813
+ */
1814
+ declare const MIN_AUTH_SECRET_LENGTH = 32;
1815
+ /**
1816
+ * Audit the Worker `env` for deployment-level security misconfigurations the
1817
+ * Durable Object can observe directly. Pure and side-effect-free — same `env`,
1818
+ * same findings — so the rules unit-test without a live shard.
1819
+ *
1820
+ * This is the server half of the studio's **Security Advisor**: CF's dashboard
1821
+ * is infra-level and can't reason about lunora's admin/WS gates or its
1822
+ * request-log redaction policy, so these are signals only lunora can surface.
1823
+ * The audit is served behind the same admin gate as every other introspection
1824
+ * RPC, so it only runs once a `LUNORA_ADMIN_TOKEN` is configured — which is why
1825
+ * a *missing* token is never itself a finding here (introspection is simply off).
1826
+ */
1827
+ declare const buildSecurityAudit: (rawEnv: unknown, options: {
1828
+ dev: boolean;
1829
+ }) => SecurityAuditResult;
1830
+ /**
1831
+ * One span in a folded trace, flattened for rendering: `depth` is its nesting
1832
+ * level under the root and `offsetMs` its start relative to the trace start, so
1833
+ * a waterfall row is a pure function of the record (indent by `depth`, bar from
1834
+ * `offsetMs` to `offsetMs + durationMs`) with no client-side tree math.
1835
+ */
1836
+ interface TraceSpan {
1837
+ attributes?: LogFields;
1838
+ /** Nesting level; the root span is 0. */
1839
+ depth: number;
1840
+ durationMs: number;
1841
+ error?: {
1842
+ message: string;
1843
+ type: string;
1844
+ };
1845
+ name: string;
1846
+ /** Start of this span relative to the trace's start, in ms. */
1847
+ offsetMs: number;
1848
+ ok: boolean;
1849
+ parentSpanId: string;
1850
+ spanId: string;
1851
+ }
1852
+ /** One folded trace: the dispatch plus every span recorded beneath it. */
1853
+ interface TraceSummary {
1854
+ /** Wall-clock span of the whole trace (root start → last span end). */
1855
+ durationMs: number;
1856
+ functionPath: string;
1857
+ /** False when the root or any descendant span errored. */
1858
+ ok: boolean;
1859
+ /** Display name of the trace — the root span's name. */
1860
+ rootName: string;
1861
+ shardKey?: string;
1862
+ /**
1863
+ * Spans ordered by `(offsetMs, depth)`, ready to render as waterfall rows.
1864
+ * Start time alone is not enough to order them: spans are recorded on
1865
+ * completion and `startTs` has millisecond resolution, so a parent and its
1866
+ * child routinely tie. Breaking that tie by depth makes the sequence a valid
1867
+ * pre-order traversal of the span tree, so indenting each row by its `depth`
1868
+ * yields the nesting without a separate tree walk.
1869
+ */
1870
+ spans: TraceSpan[];
1871
+ startTs: number;
1872
+ traceId: string;
1873
+ }
1874
+ /**
1875
+ * A bounded, in-memory ring of recent {@link SpanEvent}s (oldest evicted first),
1876
+ * mirroring `LogBuffer`. Spans arrive in *completion* order — a parent settles
1877
+ * after its children — so ordering is imposed by {@link foldTraces} at read
1878
+ * time rather than assumed here.
1879
+ */
1880
+ declare class SpanBuffer {
1881
+ private readonly buffer;
1882
+ private readonly capacity;
1883
+ constructor(capacity?: number);
1884
+ /** Number of spans currently buffered. */
1885
+ get size(): number;
1886
+ /** Drop every buffered span. */
1887
+ clear(): void;
1888
+ /** Snapshot of the buffered spans in insertion order. Fresh array per call. */
1889
+ entries(): SpanEvent[];
1890
+ /**
1891
+ * Whether any buffered span belongs to `traceId`. A membership test rather
1892
+ * than `entries().some(...)` so the per-dispatch check that decides whether
1893
+ * to record a root span doesn't copy the whole ring on every request.
1894
+ */
1895
+ hasTrace(traceId: string): boolean;
1896
+ /** Append a span, evicting the oldest when at capacity. */
1897
+ push(span: SpanEvent): void;
1898
+ }
1899
+ /** {@link foldTraces} result: the folded waterfalls plus the total distinct traces available before the `limit`. */
1900
+ interface FoldedTraces {
1901
+ /**
1902
+ * Distinct traces present in the buffer — the denominator for a "showing N of
1903
+ * M" affordance. `traces.length` is `min(total, limit)`, so `total > traces.length`
1904
+ * means older traces are held in the ring but not returned.
1905
+ */
1906
+ total: number;
1907
+ /** The newest `limit` traces, folded into waterfalls. */
1908
+ traces: TraceSummary[];
1909
+ }
1910
+ /**
1911
+ * Group a flat span list into per-trace waterfalls, newest trace first, plus the
1912
+ * total number of distinct traces available (so a caller can report truncation).
1913
+ * @param spans Buffered spans, in arrival order.
1914
+ * @param limit Maximum number of traces to return, newest first.
1915
+ */
1916
+ declare const foldTraces: (spans: ReadonlyArray<SpanEvent>, limit?: number) => FoldedTraces;
1917
+ /** One record field whose `v.storage()` value points at an object key absent from the bucket. */
1918
+ interface DanglingReference {
1919
+ /** The `v.storage()` column the dangling key was found in. */
1920
+ column: string;
1921
+ /** Primary key (`id`) of the owning row. */
1922
+ id: string;
1923
+ /** The object key the row references but which does not exist in the bucket. */
1924
+ key: string;
1925
+ /** The table the owning row lives in. */
1926
+ table: string;
1927
+ }
1928
+ /**
1929
+ * Result of {@link findDanglingReferences}: the dangling references discovered
1930
+ * (record fields pointing at a missing object), plus `truncated` — `true` when a
1931
+ * scan/result cap clipped the set, so the studio can log/surface that the view is
1932
+ * partial. `scanned` is the total number of non-empty storage-field values
1933
+ * examined, so the studio can show "checked N references".
1934
+ */
1935
+ interface DanglingReferenceResult {
1936
+ references: DanglingReference[];
1937
+ scanned: number;
1938
+ truncated: boolean;
1939
+ }
1940
+ /**
1941
+ * Find every record storage-field value that points at an object key NOT present
1942
+ * in `liveKeys` — a dangling reference (the record references a file the bucket no
1943
+ * longer has). `storageColumns` is the schema-derived `{ table: [field, …] }` map
1944
+ * the codegen subclass supplies (empty for the base, schema-free DO); `liveKeys`
1945
+ * is the set of object keys that actually exist in the bucket (the caller passes
1946
+ * the enumerated bucket listing). Scans only the declared storage columns — never
1947
+ * the whole shard — and resolves each column to its physical/`__doc__` expression
1948
+ * with the same injection-safe, bound-parameter discipline as `readTablePage`.
1949
+ *
1950
+ * Bounded: at most {@link DANGLING_SCAN_CAP} rows per column are examined and at
1951
+ * most {@link DANGLING_RESULT_CAP} references returned; `truncated` flags either
1952
+ * cap firing. An empty `storageColumns` (an app that models no storage refs)
1953
+ * yields an empty, non-truncated result.
1954
+ */
1955
+ declare const findDanglingReferences: (sql: SqlExec, storageColumns: Record<string, string[]>, liveKeys: Iterable<string>) => DanglingReferenceResult;
1956
+ /**
1957
+ * The trace ids a dispatch's spans hang off: taken from the inbound
1958
+ * `traceparent` when the runtime forwarded one (so the shard's spans join the
1959
+ * worker's trace and any container's beneath it), else freshly minted so a
1960
+ * dispatch with no inbound context — a subscription re-run, a server-initiated
1961
+ * call — still produces a coherent, self-contained local trace.
1962
+ */
1963
+ declare const resolveTraceAnchor: (traceparent: string | undefined) => {
1964
+ rootSpanId: string;
1965
+ sampled: boolean;
1966
+ traceId: string;
1967
+ };
1968
+ export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, type AiRunBinding, type AppendRequestLogEntry, type AuthMetrics, type AuthMetricsBucket, type ContextFetch, type ContextLogLevel, type ContextMetrics, type ContextTracer, DEFAULT_EXPLAIN_ISSUE_MODEL, type DanglingReference, type DanglingReferenceResult, type DatabaseInstrumentation, type DatabaseTally, type DatabaseTelemetryDeps, type ErrorIssue, type ExplainIssueArgs, type ExplainIssueDegradedReason, type ExplainIssueGrounding, type ExplainIssueResult, FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, type FoldedTraces, type FunctionMetricBucket, type FunctionMetricBucketsResult, type FunctionMetricIndexHit, type HostSpanLike, type HostTracingLike, type HostTracingResolver, ISSUE_SEVERITIES, ISSUE_STATE_TABLE, ISSUE_STATUSES, type IndexHit, type IssueSeverity, type IssueState, type IssueStatePatch, type IssueStatus, type IssuesResult, LogBuffer, type LogEntry, type LogEventInput, type LogLevel, MIN_ADMIN_TOKEN_LENGTH, MIN_AUTH_SECRET_LENGTH, MetricBuffer, type MetricEvent, type MetricHistoryOptions, type MetricHistoryPoint, type MetricHistoryResult, type MetricHistorySeries, type MetricKind, type MetricSeries, type MetricsDeps, type QueryInsightBucket, type QueryInsightEntry, type QueryInsightsResult, type QueryStatEntry, REQUEST_LOG_TABLE, type ReadIssuesOptions, type ReadRequestLogOptions, type RecordAuthEventInput, type RecordFunctionMetricInput, type RequestLogEntry, type RequestLogResult, type RequestLogWriteOptions, type RequestOutcome, type SecurityAuditResult, type SecurityFinding, type SecurityFindingKind, type SecurityFindingLevel, SpanBuffer, type SpanCollection, type SpanCollector, type SpanEvent, type SpanEventPoint, type SpanHandle, type OtlpSpanKind as SpanKind, type SpanLink, type SpanOptions, type TraceAnchor, type TraceSpan, type TraceSummary, type TracedFetchDeps, type TracerDeps, appendRequestLogEntry, buildSecurityAudit, createDatabaseTally, createMetrics, createSpanCollector, createTracedFetch, createTracer, dispatchRootSpan, emitLogEvent, emitRequestLogEvent, ensureAuthMetricsTables, ensureFunctionMetricsTables, ensureRequestLogTable, explainIssue, findDanglingReferences, foldTraces, formatTally, instrumentDatabase, mergeScanAttribution, parseExplainIssueArgs, parseLogArgs, readAuthMetrics, readErrorIssues, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, readMetricHistory, readQueryInsights, readQueryMetrics, readRequestLog, recordAuthEvent, recordFunctionMetric, recordMetricHistory, recordQueryMetric, redactArgs, resolveTraceAnchor, upsertIssueState };