@lunora/observability 0.0.0 → 1.0.0-alpha.2
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.
- package/LICENSE.md +282 -0
- package/README.md +22 -33
- package/dist/index.d.mts +1818 -0
- package/dist/index.d.ts +1818 -0
- package/dist/index.mjs +1 -0
- package/dist/packem_shared/AUTH_METRICS_BUCKETS_TABLE-C2V-DiGu.mjs +22 -0
- package/dist/packem_shared/DEFAULT_EXPLAIN_ISSUE_MODEL-xF11R4vX.mjs +3 -0
- package/dist/packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-B1U7xWMo.mjs +56 -0
- package/dist/packem_shared/ISSUE_SEVERITIES-Js6lGUG5.mjs +15 -0
- package/dist/packem_shared/LogBuffer-bIvCelI-.mjs +1 -0
- package/dist/packem_shared/MIN_ADMIN_TOKEN_LENGTH-T-zuwRVR.mjs +1 -0
- package/dist/packem_shared/MetricBuffer-CYrLuEsz.mjs +1 -0
- package/dist/packem_shared/REQUEST_LOG_TABLE-Q_BYgAzY.mjs +20 -0
- package/dist/packem_shared/SpanBuffer-BR6Ff0M-.mjs +1 -0
- package/dist/packem_shared/createDatabaseTally-BTqRpC0j.mjs +1 -0
- package/dist/packem_shared/createMetrics-fpVig63l.mjs +1 -0
- package/dist/packem_shared/findDanglingReferences-D-x9LvY0.mjs +1 -0
- package/dist/packem_shared/log-fields-d5ouN1Pe.mjs +1 -0
- package/dist/packem_shared/metric-buffer-CdgXal7w.mjs +1 -0
- package/dist/packem_shared/readMetricHistory-DtnhufUq.mjs +31 -0
- package/dist/packem_shared/readQueryInsights-CwSKB0I1.mjs +32 -0
- package/dist/packem_shared/resolveTraceAnchor-IAN8riI0.mjs +1 -0
- package/dist/packem_shared/trace-context-DrdF960P.mjs +1 -0
- package/package.json +51 -7
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1818 @@
|
|
|
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
|
+
/** Function path the spans are attributed to. */
|
|
492
|
+
functionPath: string;
|
|
493
|
+
/**
|
|
494
|
+
* **Opt-in, EXPERIMENTAL, default off.** When `true` *and*
|
|
495
|
+
* {@link TracerDeps.resolveHostTracing} yields a working
|
|
496
|
+
* `tracing.enterSpan`, each `ctx.trace` span is ALSO emitted as a host-native
|
|
497
|
+
* **custom span**, so it nests inside CF's native binding/fetch/handler trace
|
|
498
|
+
* tree on the hosted path. This only ADDS a CF-side span — the recorded
|
|
499
|
+
* {@link SpanEvent} (our `SpanBuffer`/`otlpSink`) is untouched and remains the
|
|
500
|
+
* source of truth plus the local studio waterfall. The `enterSpan` call itself
|
|
501
|
+
* is now workerd-validated as available and side-effect-free inside a Durable
|
|
502
|
+
* Object; CF's exported parent-linking under sampling remains unverified. See
|
|
503
|
+
* {@link createTracer} for the double-export and Durable-Object async-context
|
|
504
|
+
* caveats.
|
|
505
|
+
*/
|
|
506
|
+
fuseHostSpans?: boolean;
|
|
507
|
+
/** Hand a finished span to the buffer + sink. */
|
|
508
|
+
record: (span: SpanEvent) => void;
|
|
509
|
+
/**
|
|
510
|
+
* Injected resolver for CF's `tracing` namespace (see
|
|
511
|
+
* {@link HostTracingResolver}). Only consulted when
|
|
512
|
+
* {@link TracerDeps.fuseHostSpans} is `true`, so the default path never
|
|
513
|
+
* calls it.
|
|
514
|
+
*/
|
|
515
|
+
resolveHostTracing?: HostTracingResolver;
|
|
516
|
+
/** Shard key for single-shard calls; absent for the unnamed root DO. */
|
|
517
|
+
shardKey: string | undefined;
|
|
518
|
+
/** Read lazily — the acting user is resolved per span, not per ctx. */
|
|
519
|
+
userId: () => string | undefined;
|
|
520
|
+
}
|
|
521
|
+
/** What {@link createMetrics} needs from the shard to build a measurement. */
|
|
522
|
+
interface MetricsDeps {
|
|
523
|
+
functionPath: string;
|
|
524
|
+
record: (event: MetricEvent) => void;
|
|
525
|
+
shardKey: string | undefined;
|
|
526
|
+
}
|
|
527
|
+
/** Everything a {@link SpanHandle}'s body attached, ready to merge into the recorded span. */
|
|
528
|
+
interface SpanCollection {
|
|
529
|
+
attributes: Record<string, LogFields[string]>;
|
|
530
|
+
events: SpanEventPoint[];
|
|
531
|
+
links: SpanLink[];
|
|
532
|
+
}
|
|
533
|
+
/** A {@link SpanHandle} plus read access to what it has collected so far. */
|
|
534
|
+
interface SpanCollector {
|
|
535
|
+
collected: SpanCollection;
|
|
536
|
+
handle: SpanHandle;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Build a span's post-hoc collection surface: the {@link SpanHandle} handed to a
|
|
540
|
+
* `ctx.trace` body, and the bag it writes into.
|
|
541
|
+
*
|
|
542
|
+
* Factored out because the same surface backs two things — every `ctx.trace`
|
|
543
|
+
* span, and the per-dispatch **wide event** (`ctx.span`), where the accumulated
|
|
544
|
+
* attributes become the canonical one-event-per-request summary. Sharing the
|
|
545
|
+
* implementation is what makes those two feel like the same API instead of two
|
|
546
|
+
* that happen to resemble each other.
|
|
547
|
+
*/
|
|
548
|
+
declare const createSpanCollector: (ids: {
|
|
549
|
+
spanId: string;
|
|
550
|
+
traceId: string;
|
|
551
|
+
}) => SpanCollector;
|
|
552
|
+
/**
|
|
553
|
+
* Build the `ctx.trace` span factory for one dispatched function.
|
|
554
|
+
*
|
|
555
|
+
* **Nesting is explicit, not ambient.** Each span's body receives a tracer bound
|
|
556
|
+
* to that span; calling it is what makes a child. An earlier design kept an
|
|
557
|
+
* ambient stack of "the currently open span" and parented to its top, which
|
|
558
|
+
* reads nicer but is unfixably wrong under concurrency: in
|
|
559
|
+
* `Promise.all([trace("a", …), trace("b", …)])`, `b` starts while `a` is on the
|
|
560
|
+
* stack and is recorded as a *child* of `a` rather than its sibling — and
|
|
561
|
+
* parallel fan-out is one of the main things people reach for a tracer to
|
|
562
|
+
* measure. Distinguishing "called inside a's body" from "called concurrently
|
|
563
|
+
* with a" needs `AsyncLocalStorage`, which this package deliberately avoids (see
|
|
564
|
+
* `dependency-tracker.ts` — shard DOs run under a slimmer compat profile than
|
|
565
|
+
* `nodejs_compat`). So the parent is threaded, exactly like the dependency
|
|
566
|
+
* tracker and the subscription identity: correct in every case, and visible at
|
|
567
|
+
* the call site.
|
|
568
|
+
*
|
|
569
|
+
* The anchor is passed in for the same reason. `ShardDO.currentRequestTrace` is
|
|
570
|
+
* cleared in the dispatch `finally`, and a subscription re-run builds its ctx
|
|
571
|
+
* during* the writing mutation's flush — so reading that shared field at span
|
|
572
|
+
* time would file the re-run's spans under the mutation's trace.
|
|
573
|
+
*
|
|
574
|
+
* **Cloudflare custom-spans bridge (opt-in, EXPERIMENTAL).** When
|
|
575
|
+
* `deps.fuseHostSpans` is `true` and `deps.resolveHostTracing` yields
|
|
576
|
+
* a working `tracing.enterSpan` (`cloudflare:workers`, GA 2026-06-16), each span
|
|
577
|
+
* body runs inside a CF custom span so our span nests under CF's native
|
|
578
|
+
* binding/fetch/handler trace tree on the hosted path, and the finished span's
|
|
579
|
+
* key attributes are mirrored onto it (gated on `span.isTraced`). Two deliberate
|
|
580
|
+
* boundaries hold.
|
|
581
|
+
*
|
|
582
|
+
* **No double-export by default, and never a replacement.** The bridge only ADDS a
|
|
583
|
+
* CF-side span; the recorded {@link SpanEvent} handed to `record` (our
|
|
584
|
+
* `SpanBuffer`/`otlpSink`) is byte-for-byte the same as without the bridge and
|
|
585
|
+
* stays the source of truth. It is off unless explicitly enabled precisely
|
|
586
|
+
* because, once on, a deployment that ALSO ships our `otlpSink` to a collector
|
|
587
|
+
* AND lets CF export its trace tree emits the same logical span down two
|
|
588
|
+
* pipelines — an intentional, documented trade the operator opts into, not a
|
|
589
|
+
* default.
|
|
590
|
+
*
|
|
591
|
+
* **DO async-context caveat (EXPERIMENTAL, partially workerd-validated).**
|
|
592
|
+
* `tracing.enterSpan` is now confirmed to EXIST and RUN inside a real Durable
|
|
593
|
+
* Object under `@cloudflare/vitest-pool-workers` (see
|
|
594
|
+
* `__tests__/workerd/context-telemetry-cf-bridge.workerd.test.ts`): it resolves
|
|
595
|
+
* from `cloudflare:workers`, its callback executes and returns the body value
|
|
596
|
+
* without throwing, `span.isTraced` is a real boolean, and — the key additive
|
|
597
|
+
* guarantee — our recorded {@link SpanEvent} tree (parent/child via the threaded
|
|
598
|
+
* `parentSpanId`) is byte-for-byte identical with the bridge on vs off. What that
|
|
599
|
+
* harness CANNOT prove is CF's own *exported* parent-linking: with no trace head
|
|
600
|
+
* attached the run is unsampled (`isTraced === false`), so CF records nothing and
|
|
601
|
+
* its span tree is not introspectable. So `enterSpan`'s ambient-span parent-linking
|
|
602
|
+
* inside a DO stays unverified upstream, and this remains capability-probed: an
|
|
603
|
+
* absent/undefined `tracing`, a missing `enterSpan`, or an off-CF/unsampled run all
|
|
604
|
+
* resolve to `undefined`/no-op — exact prior behavior. If CF's ambient-span linkage
|
|
605
|
+
* misbehaves in a DO, the worst case is a mis-parented CF span; our own recorded
|
|
606
|
+
* waterfall is unaffected.
|
|
607
|
+
*/
|
|
608
|
+
declare const createTracer: (deps: TracerDeps) => ContextTracer;
|
|
609
|
+
/** What {@link createTracedFetch} needs from the shard. */
|
|
610
|
+
interface TracedFetchDeps {
|
|
611
|
+
/** The trace the CLIENT spans belong to. */
|
|
612
|
+
anchor: TraceAnchor;
|
|
613
|
+
/** Function path the spans are attributed to. */
|
|
614
|
+
functionPath: string;
|
|
615
|
+
/**
|
|
616
|
+
* Whether to inject `traceparent` into the outbound request — and, with a
|
|
617
|
+
* predicate, to which destinations. Default `true`.
|
|
618
|
+
*/
|
|
619
|
+
propagate?: ((url: URL) => boolean) | boolean;
|
|
620
|
+
/** Hand a finished span to the buffer + sink. */
|
|
621
|
+
record: (span: SpanEvent) => void;
|
|
622
|
+
/** Shard key for single-shard calls; absent for the unnamed root DO. */
|
|
623
|
+
shardKey: string | undefined;
|
|
624
|
+
/** Read lazily — the acting user is resolved per span. */
|
|
625
|
+
userId: () => string | undefined;
|
|
626
|
+
}
|
|
627
|
+
/** The `fetch` shape `ctx.fetch` exposes — the platform global's, narrowed to what we wrap. */
|
|
628
|
+
type ContextFetch = (input: Request | string | URL, init?: RequestInit) => Promise<Response>;
|
|
629
|
+
/**
|
|
630
|
+
* Build `ctx.fetch`: the platform `fetch`, wrapped so every outbound call
|
|
631
|
+
* becomes a **CLIENT span** and carries W3C trace context to the callee.
|
|
632
|
+
*
|
|
633
|
+
* Two gaps close here. First, an uninstrumented `fetch` makes the single most
|
|
634
|
+
* common source of latency — waiting on somebody else's service — invisible: a
|
|
635
|
+
* handler that spends 900ms in Stripe shows one opaque 900ms bar. Second,
|
|
636
|
+
* without an outbound `traceparent` the callee starts a brand-new trace, so the
|
|
637
|
+
* two halves of one logical request can never be stitched together, which is the
|
|
638
|
+
* entire premise of distributed tracing.
|
|
639
|
+
*
|
|
640
|
+
* The span id is minted BEFORE the request is sent, precisely so the header
|
|
641
|
+
* announces the id the span will actually be recorded under. Deriving it
|
|
642
|
+
* afterwards (or reusing the parent's) would produce a `traceparent` naming a
|
|
643
|
+
* span that never existed, and a callee parented to nothing.
|
|
644
|
+
*
|
|
645
|
+
* Kind is `client` rather than `internal` — that is what lets a collector draw
|
|
646
|
+
* the edge to the downstream service in a service map.
|
|
647
|
+
*
|
|
648
|
+
* Failures are recorded and re-thrown untouched, and a non-2xx response is
|
|
649
|
+
* recorded as an ERROR span (it is a failed call from the caller's point of
|
|
650
|
+
* view) while still being returned normally — instrumentation, never flow
|
|
651
|
+
* control.
|
|
652
|
+
*/
|
|
653
|
+
declare const createTracedFetch: (deps: TracedFetchDeps, base: ContextFetch) => ContextFetch;
|
|
654
|
+
/**
|
|
655
|
+
* Build the `ctx.metrics` recorder for one dispatched function.
|
|
656
|
+
*
|
|
657
|
+
* Deliberately stateless: each call emits one measurement rather than
|
|
658
|
+
* accumulating into a per-dispatch map. Pre-aggregating here would have to pick a
|
|
659
|
+
* flush point and a merge rule per instrument kind (sum a counter, last-wins a
|
|
660
|
+
* gauge, and a histogram cannot be merged at all without losing the
|
|
661
|
+
* distribution) — so the runtime stays a transport and the collector, which is
|
|
662
|
+
* built for exactly this, does the aggregation.
|
|
663
|
+
*/
|
|
664
|
+
declare const createMetrics: (deps: MetricsDeps) => ContextMetrics;
|
|
665
|
+
/**
|
|
666
|
+
* Build the synthetic root span for a finished dispatch — the bar the studio's
|
|
667
|
+
* waterfall hangs a request's `ctx.trace` spans under.
|
|
668
|
+
*
|
|
669
|
+
* Pure: the caller decides whether to record it (only when the dispatch actually
|
|
670
|
+
* produced spans) and where to put it. It is never routed to `sink.onSpan`,
|
|
671
|
+
* because the runtime already emits the dispatch to `onRpc` and a collector would
|
|
672
|
+
* otherwise show it twice.
|
|
673
|
+
*/
|
|
674
|
+
declare const dispatchRootSpan: (input: {
|
|
675
|
+
anchor: TraceAnchor;
|
|
676
|
+
/**
|
|
677
|
+
* What the handler attached to the dispatch through `ctx.span` — the **wide
|
|
678
|
+
* event**. These are the attributes that would otherwise have been scattered
|
|
679
|
+
* across a dozen `ctx.log` lines; carrying them on the one span that already
|
|
680
|
+
* exists per request is the OTel-native way to get a wide event without
|
|
681
|
+
* multiplying log records.
|
|
682
|
+
*/
|
|
683
|
+
collected?: SpanCollection;
|
|
684
|
+
durationMs: number;
|
|
685
|
+
failure: {
|
|
686
|
+
thrown: unknown;
|
|
687
|
+
} | undefined;
|
|
688
|
+
functionPath: string;
|
|
689
|
+
shardKey: string | undefined;
|
|
690
|
+
startTs: number;
|
|
691
|
+
userId: string | undefined;
|
|
692
|
+
}) => SpanEvent;
|
|
693
|
+
/** Running totals for `"summary"` mode; created by the caller, read once at the dispatch boundary. */
|
|
694
|
+
interface DatabaseTally {
|
|
695
|
+
calls: number;
|
|
696
|
+
durationMs: number;
|
|
697
|
+
errors: number;
|
|
698
|
+
perOperation: Record<string, number>;
|
|
699
|
+
spansEmitted: number;
|
|
700
|
+
spansTruncated: boolean;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* How much detail `ctx.db` auto-instrumentation produces.
|
|
704
|
+
*
|
|
705
|
+
* `"summary"` (default) — aggregate counters on the dispatch's wide event: no
|
|
706
|
+
* extra spans, no extra log records, and a cost that does not grow with call count.
|
|
707
|
+
*
|
|
708
|
+
* `"spans"` — one span per database call. The full waterfall, at the price of a
|
|
709
|
+
* span per call; right when diagnosing, noisy as a permanent default.
|
|
710
|
+
*
|
|
711
|
+
* `"off"` — no database telemetry at all.
|
|
712
|
+
*/
|
|
713
|
+
type DatabaseInstrumentation = "off" | "spans" | "summary";
|
|
714
|
+
/** What {@link instrumentDatabase} needs to record what it observes. */
|
|
715
|
+
interface DatabaseTelemetryDeps {
|
|
716
|
+
/** The trace produced spans belong to (`"spans"` mode only). */
|
|
717
|
+
anchor: TraceAnchor;
|
|
718
|
+
/** Function path spans and attributes are attributed to. */
|
|
719
|
+
functionPath: string;
|
|
720
|
+
/** Detail level; see {@link DatabaseInstrumentation}. */
|
|
721
|
+
mode: DatabaseInstrumentation;
|
|
722
|
+
/** Hand a finished span to the buffer + sink (`"spans"` mode only). */
|
|
723
|
+
record: (span: SpanEvent) => void;
|
|
724
|
+
/** Shard key for single-shard calls; absent for the unnamed root DO. */
|
|
725
|
+
shardKey: string | undefined;
|
|
726
|
+
/**
|
|
727
|
+
* Caller-supplied accumulator for `"summary"` mode. The instrumenter only ever
|
|
728
|
+
* increments numbers on it; the shard reads it ONCE at the dispatch boundary
|
|
729
|
+
* and formats it with {@link formatTally}.
|
|
730
|
+
*
|
|
731
|
+
* Two properties fall out of that split. Per-call cost stays at a few integer
|
|
732
|
+
* increments — no object allocation, no lookup — which matters because this is
|
|
733
|
+
* on the path of every query. And because nothing is written through the
|
|
734
|
+
* dispatch's `SpanHandle`, the wide-event collector is never materialized, so
|
|
735
|
+
* a handler that instrumented nothing still doesn't trip the root-span gate.
|
|
736
|
+
*/
|
|
737
|
+
tally: DatabaseTally;
|
|
738
|
+
/** Read lazily — the acting user is resolved per span. */
|
|
739
|
+
userId: () => string | undefined;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Wrap a `ctx.db` writer so its storage-touching methods are instrumented.
|
|
743
|
+
*
|
|
744
|
+
* Returns the database unchanged when `mode` is `"off"`, so the default-disabled
|
|
745
|
+
* path costs nothing — not even a proxy indirection.
|
|
746
|
+
*
|
|
747
|
+
* Implemented as a `Proxy` rather than by enumerating and rebinding methods:
|
|
748
|
+
* `DatabaseWriterLike` has optional members that a given backend may or may not
|
|
749
|
+
* implement, plus properties (`system`) and builder factories (`query`) that
|
|
750
|
+
* must pass through untouched. A proxy instruments exactly what it is asked to
|
|
751
|
+
* and is transparently correct for everything else, including members added
|
|
752
|
+
* later — an enumeration would silently stop covering them.
|
|
753
|
+
*/
|
|
754
|
+
declare const instrumentDatabase: <T extends object>(database: T, deps: DatabaseTelemetryDeps) => T;
|
|
755
|
+
/** A zero'd tally for one dispatch. */
|
|
756
|
+
declare const createDatabaseTally: () => DatabaseTally;
|
|
757
|
+
/**
|
|
758
|
+
* Render the running tally as span attributes.
|
|
759
|
+
*
|
|
760
|
+
* Called ONCE per dispatch, from the shard's root-span recorder — not per query.
|
|
761
|
+
* 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
|
|
762
|
+
* assignment — the property that makes `"summary"` mode scale to any call count.
|
|
763
|
+
*/
|
|
764
|
+
declare const formatTally: (tally: DatabaseTally) => LogFields;
|
|
765
|
+
/** Reserved per-function accumulator table. Auto-hidden from the data browser by the `__lunora` prefix. */
|
|
766
|
+
declare const FUNCTION_METRICS_TABLE = "__lunora_metrics";
|
|
767
|
+
/** Reserved coarse time-series table: per-function call/error counts bucketed by a fixed window. */
|
|
768
|
+
declare const FUNCTION_METRICS_BUCKETS_TABLE = "__lunora_metrics_buckets";
|
|
769
|
+
/** Reserved causal full-scan attribution table: per-(function, table) full-scan counts. */
|
|
770
|
+
declare const FUNCTION_METRICS_SCANS_TABLE = "__lunora_metrics_scans";
|
|
771
|
+
/** Reserved per-(table, index) hit-counter table backing the advisor dead-index lint. */
|
|
772
|
+
declare const FUNCTION_METRICS_INDEX_TABLE = "__lunora_metrics_index";
|
|
773
|
+
/**
|
|
774
|
+
* Width of one history bucket, in milliseconds. 60s gives a minute-resolution
|
|
775
|
+
* time series — fine-grained enough to chart bursts on the studio, coarse
|
|
776
|
+
* enough that a single function emits at most one row per minute. Exported so
|
|
777
|
+
* consumers (and tests) can align timestamps to the same grid.
|
|
778
|
+
*/
|
|
779
|
+
declare const FUNCTION_METRICS_BUCKET_MS = 6e4;
|
|
780
|
+
/**
|
|
781
|
+
* Most recent buckets kept per function; older rows are trimmed after each
|
|
782
|
+
* write so the time series can't grow unbounded. 1440 minute-buckets ≈ 24h of
|
|
783
|
+
* history per function.
|
|
784
|
+
*/
|
|
785
|
+
declare const FUNCTION_METRICS_BUCKET_RETENTION = 1440;
|
|
786
|
+
/**
|
|
787
|
+
* Maximum distinct function `path`s tracked in the accumulator table. Mirrors
|
|
788
|
+
* `query-metrics.ts`'s `QUERY_METRICS_MAX_STATEMENTS` cap (and exists for the
|
|
789
|
+
* same reason): the `path` is attacker-reachable — an unregistered/`FUNCTION_NOT_FOUND`
|
|
790
|
+
* dispatch still records a row keyed by the caller-supplied `functionPath` — so
|
|
791
|
+
* without a cap a flood of distinct random paths would grow `__lunora_metrics`
|
|
792
|
+
* (and its bucket/scan satellites) without bound, eventually filling the shard's
|
|
793
|
+
* SQLite store shared with the app's real data. A few thousand registered
|
|
794
|
+
* functions is already far beyond any real app, so a new path past this cap is
|
|
795
|
+
* dropped while already-tracked paths keep accumulating.
|
|
796
|
+
*/
|
|
797
|
+
declare const FUNCTION_METRICS_MAX_PATHS = 5e3;
|
|
798
|
+
/**
|
|
799
|
+
* Upper bound on rows the admin reads materialize into DO memory at once. Even
|
|
800
|
+
* with the write-side `FUNCTION_METRICS_MAX_PATHS` cap in place, an existing
|
|
801
|
+
* shard could already hold a bloated accumulator (rows written before the cap
|
|
802
|
+
* landed), so the read path also clamps — a `SELECT *` with no LIMIT would
|
|
803
|
+
* otherwise load every row via `.toArray()` and risk OOMing the ~128MB DO when
|
|
804
|
+
* the studio Function Stats panel opens. Ordered reads keep the busiest/most
|
|
805
|
+
* recent rows; the tail past this limit is simply not returned to the panel.
|
|
806
|
+
*/
|
|
807
|
+
declare const FUNCTION_METRICS_READ_LIMIT = 1e3;
|
|
808
|
+
/** One coarse time-series sample for a function: call/error counts within `[bucketMs, bucketMs + FUNCTION_METRICS_BUCKET_MS)`. */
|
|
809
|
+
interface FunctionMetricBucket {
|
|
810
|
+
/** Epoch-ms floor of the bucket window. */
|
|
811
|
+
bucketMs: number;
|
|
812
|
+
/** Dispatches recorded in this window. */
|
|
813
|
+
calls: number;
|
|
814
|
+
/** Subset of `calls` that threw. */
|
|
815
|
+
errors: number;
|
|
816
|
+
}
|
|
817
|
+
/** One declared index a dispatch exercised (used to narrow a read). */
|
|
818
|
+
interface IndexHit {
|
|
819
|
+
/** The declared index name. */
|
|
820
|
+
index: string;
|
|
821
|
+
/** The table the index is declared on. */
|
|
822
|
+
table: string;
|
|
823
|
+
}
|
|
824
|
+
/** One declared index's cumulative recorded read count (durable, non-decaying) — the advisor dead-index lint input. */
|
|
825
|
+
interface FunctionMetricIndexHit {
|
|
826
|
+
/** The declared index name. */
|
|
827
|
+
index: string;
|
|
828
|
+
/** Recorded reads that used this index to narrow. */
|
|
829
|
+
reads: number;
|
|
830
|
+
/** The table the index is declared on. */
|
|
831
|
+
table: string;
|
|
832
|
+
}
|
|
833
|
+
/** Fields recorded for one completed dispatch. `errored` advances the failure counters. */
|
|
834
|
+
interface RecordFunctionMetricInput {
|
|
835
|
+
/**
|
|
836
|
+
* Whether the dispatch failed on an optimistic-concurrency (OCC) write
|
|
837
|
+
* conflict — a compare-and-swap that lost to a concurrent commit. Advances
|
|
838
|
+
* the durable `conflicts` counter behind the write-contention advisor. A
|
|
839
|
+
* conflicted dispatch also `errored`, so conflicts are a subset of errors.
|
|
840
|
+
* Omitted/false on the common path, keeping the hot path unchanged.
|
|
841
|
+
*/
|
|
842
|
+
conflicted?: boolean;
|
|
843
|
+
/** Wall-clock millis the handler took. */
|
|
844
|
+
durationMs: number;
|
|
845
|
+
/** Whether the dispatch threw. */
|
|
846
|
+
errored: boolean;
|
|
847
|
+
/** Most recent failure message, recorded only when `errored`. */
|
|
848
|
+
errorMessage?: string;
|
|
849
|
+
/**
|
|
850
|
+
* Distinct declared indexes this dispatch exercised (used to narrow a read),
|
|
851
|
+
* collected from the `onIndexUse` signal. Each entry bumps the
|
|
852
|
+
* per-`(table, index)` hit counter in `__lunora_metrics_index`, the durable
|
|
853
|
+
* producer behind the advisor dead-index lint. Omitted/empty when the
|
|
854
|
+
* dispatch used no declared index, keeping the hot path unchanged.
|
|
855
|
+
*/
|
|
856
|
+
indexHits?: ReadonlyArray<IndexHit>;
|
|
857
|
+
/** The `<file>:<function>` identifier. */
|
|
858
|
+
path: string;
|
|
859
|
+
/**
|
|
860
|
+
* Distinct tables this dispatch full-scanned (read with no index / point
|
|
861
|
+
* lookup), collected from the `SCAN_DEP` reads. Each entry bumps the
|
|
862
|
+
* aggregate `scans` counter and the per-`(path, table)` attribution row.
|
|
863
|
+
* Omitted/empty when the dispatch didn't full-scan anything (the common
|
|
864
|
+
* indexed case), keeping the hot path to the same two upserts as before.
|
|
865
|
+
*/
|
|
866
|
+
scannedTables?: ReadonlyArray<string>;
|
|
867
|
+
/** Epoch-ms the dispatch completed. */
|
|
868
|
+
ts: number;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Create the four reserved metrics tables. Idempotent, so the read and write
|
|
872
|
+
* paths can call it defensively. The accumulator table is keyed by `path` (one
|
|
873
|
+
* row per function); the bucket table by `(path, bucketMs)` (one row per
|
|
874
|
+
* function per window); the scans table by `(path, table)` (one row per
|
|
875
|
+
* function per full-scanned table); the index table by `(table, index)` (one
|
|
876
|
+
* row per declared index the app exercises).
|
|
877
|
+
*
|
|
878
|
+
* The accumulator's `scans` column is added via a guarded `ALTER TABLE` rather
|
|
879
|
+
* than baked into the `CREATE` so a shard whose `__lunora_metrics` predates the
|
|
880
|
+
* causal-attribution feature gains the column on the next call without a
|
|
881
|
+
* migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
|
|
882
|
+
* error from a re-run is swallowed.
|
|
883
|
+
*/
|
|
884
|
+
declare const ensureFunctionMetricsTables: (sql: SqlExec) => void;
|
|
885
|
+
/**
|
|
886
|
+
* Persist one completed dispatch: a single upsert into the accumulator row and
|
|
887
|
+
* one upsert into the current time bucket, then a bounded trim of old buckets
|
|
888
|
+
* for that path. Creates the tables first so callers needn't. This is the hot
|
|
889
|
+
* path — exactly two `INSERT … ON CONFLICT … DO UPDATE` statements plus a
|
|
890
|
+
* bounded `DELETE`, all keyed by primary key, so it stays cheap.
|
|
891
|
+
*
|
|
892
|
+
* When the dispatch full-scanned one or more tables (`scannedTables`), the
|
|
893
|
+
* aggregate `scans` counter on the accumulator row advances by the distinct
|
|
894
|
+
* table count and one extra `(path, table)` upsert fires per scanned table.
|
|
895
|
+
* Indexed dispatches (the common case) skip all of that and pay nothing.
|
|
896
|
+
*/
|
|
897
|
+
declare const recordFunctionMetric: (sql: SqlExec, input: RecordFunctionMetricInput) => void;
|
|
898
|
+
/**
|
|
899
|
+
* Read the per-function full-scan attribution, grouped by function path. The
|
|
900
|
+
* returned map keys are `path`; each value is the function's full-scanned
|
|
901
|
+
* tables ordered by scan count (busiest scan first), so the causal "slow
|
|
902
|
+
* BECAUSE it scanned X" read can lead with the dominant table. Creates the
|
|
903
|
+
* table first so reads on a never-called shard return an empty map.
|
|
904
|
+
*/
|
|
905
|
+
declare const readFunctionMetricScans: (sql: SqlExec) => Map<string, FunctionScanAttribution[]>;
|
|
906
|
+
/**
|
|
907
|
+
* Read the per-`(table, index)` hit counts — the advisor dead-index lint input.
|
|
908
|
+
* Each entry is a declared index and how many recorded reads used it to narrow
|
|
909
|
+
* (a cumulative, non-decaying count); the lint reconciles this against the schema
|
|
910
|
+
* to flag a declared index that appears with zero reads (or not at all) as dead. Ordered
|
|
911
|
+
* by table then index for stable output. Creates the table first so a read on a
|
|
912
|
+
* never-exercised shard returns `[]`.
|
|
913
|
+
*/
|
|
914
|
+
declare const readFunctionMetricIndexHits: (sql: SqlExec) => FunctionMetricIndexHit[];
|
|
915
|
+
/**
|
|
916
|
+
* Fold a dispatch's distinct full-scanned tables into an in-memory attribution
|
|
917
|
+
* list, mirroring the per-`(path, table)` upsert {@link recordFunctionMetric}
|
|
918
|
+
* applies to the durable `__lunora_metrics_scans` table. Kept here, beside its
|
|
919
|
+
* SQL twin, so the one rule (one occurrence = +1 scan for that table, list
|
|
920
|
+
* re-sorted busiest-first) lives in a single module — the in-memory copy exists
|
|
921
|
+
* only for the warm-instance fallback when the durable read is unavailable.
|
|
922
|
+
* Mutates and returns `into`.
|
|
923
|
+
*/
|
|
924
|
+
declare const mergeScanAttribution: (into: FunctionScanAttribution[], scanned: ReadonlyArray<string>) => FunctionScanAttribution[];
|
|
925
|
+
/**
|
|
926
|
+
* Read the persisted per-function accumulators as {@link FunctionCallStat}s,
|
|
927
|
+
* newest-called first. Creates the table first so reads on a never-called shard
|
|
928
|
+
* return `[]` instead of throwing. The shape is a superset of the legacy
|
|
929
|
+
* in-memory `getFunctionStats` rows — the additive `scans` total and
|
|
930
|
+
* `scannedTables` causal attribution are folded in here so a single read backs
|
|
931
|
+
* the Insights "missing index" / "full scan" signal.
|
|
932
|
+
*/
|
|
933
|
+
declare const readFunctionMetrics: (sql: SqlExec) => FunctionCallStat[];
|
|
934
|
+
/**
|
|
935
|
+
* Read the coarse time-series buckets for `path` (every path when omitted),
|
|
936
|
+
* oldest-bucket first so a chart can plot them left-to-right. Creates the table
|
|
937
|
+
* first so reads on a never-called shard return `[]`.
|
|
938
|
+
*/
|
|
939
|
+
declare const readFunctionMetricBuckets: (sql: SqlExec, path?: string) => (FunctionMetricBucket & {
|
|
940
|
+
path: string;
|
|
941
|
+
})[];
|
|
942
|
+
/**
|
|
943
|
+
* Aggregate the persisted accumulators into the lifetime totals the metrics
|
|
944
|
+
* health snapshot reports: total calls (`requests`), total `errors`, and the
|
|
945
|
+
* earliest `last_called_at` seen — a best-effort "since" marker for durable
|
|
946
|
+
* data. Returns zeroes on a never-called shard.
|
|
947
|
+
*/
|
|
948
|
+
declare const readFunctionMetricsTotals: (sql: SqlExec) => {
|
|
949
|
+
errors: number;
|
|
950
|
+
requests: number;
|
|
951
|
+
};
|
|
952
|
+
/**
|
|
953
|
+
* The Workers AI text model the Issue explainer uses when the caller does not
|
|
954
|
+
* override it. The fp8-fast instruct model the rest of the repo defaults to —
|
|
955
|
+
* the explainer is a short, grounded rewrite (not a reasoning task), so a
|
|
956
|
+
* latency-optimized build beats a larger one. Deliberately not the retired
|
|
957
|
+
* `@cf/meta/llama-3.1-8b-instruct`: a deprecated model-id makes `binding.run`
|
|
958
|
+
* throw, which would silently degrade every explain to `"ai-error"`.
|
|
959
|
+
*/
|
|
960
|
+
/**
|
|
961
|
+
* Fallback model id when neither the caller nor the request names one.
|
|
962
|
+
*
|
|
963
|
+
* A Workers AI id, which makes it the *host's* default rather than this
|
|
964
|
+
* package's — a second host runs different models and must be able to say so.
|
|
965
|
+
* `explainIssue` therefore takes `defaultModel`, and this constant is only what
|
|
966
|
+
* the Cloudflare host happens to pass. Kept exported so that host has a name to
|
|
967
|
+
* pass rather than a string literal.
|
|
968
|
+
*/
|
|
969
|
+
declare const DEFAULT_EXPLAIN_ISSUE_MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
|
|
970
|
+
/**
|
|
971
|
+
* Structural projection of the Workers `AI` binding's `run` method — declared
|
|
972
|
+
* locally so `@lunora/do` needs no dependency edge on `@lunora/ai` (nor on
|
|
973
|
+
* `@cloudflare/workers-types`) to reach `env.AI`. Mirrors `AiBindingLike` in
|
|
974
|
+
* `@lunora/ai`.
|
|
975
|
+
*/
|
|
976
|
+
interface AiRunBinding {
|
|
977
|
+
run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
978
|
+
}
|
|
979
|
+
/** Parsed `__lunora_admin__:explainIssue` payload: the folded Issue's identifying facts, plus an optional model override. */
|
|
980
|
+
interface ExplainIssueArgs {
|
|
981
|
+
/** The Issue's culprit (`<file>:<function>` or `container:<name>`), for grounding context. */
|
|
982
|
+
culprit?: string;
|
|
983
|
+
/** Per-request model-id override; falls back to the caller's `defaultModel`, then {@link DEFAULT_EXPLAIN_ISSUE_MODEL}. */
|
|
984
|
+
model?: string;
|
|
985
|
+
/** A representative raw error message for the Issue — the fact the explanation is grounded in. Required. */
|
|
986
|
+
sampleMessage: string;
|
|
987
|
+
/** The Issue's human-readable title (first line of the sample message), for grounding context. */
|
|
988
|
+
title?: string;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Why the explainer fell back to the grounded hint alone. A closed union rather
|
|
992
|
+
* than a bare `string` so `degraded(reason)` rejects a typo'd sentinel at compile
|
|
993
|
+
* time instead of letting it fall through to the client's generic error copy.
|
|
994
|
+
* Mirrored by `ExplainIssueResult["reason"]` in `@lunora/studio`'s `lib/admin.ts`.
|
|
995
|
+
*/
|
|
996
|
+
type ExplainIssueDegradedReason = "ai-error" | "empty-response" | "no-ai-binding";
|
|
997
|
+
/**
|
|
998
|
+
* The grounding facts both `__lunora_admin__:explainIssue` outcomes carry. Present
|
|
999
|
+
* whenever {@link findIssueSolution} recognized the message — offline,
|
|
1000
|
+
* deterministic, and independent of whether the AI path ran at all.
|
|
1001
|
+
*
|
|
1002
|
+
* The hint BODY is deliberately not on the wire: the client derives it from the
|
|
1003
|
+
* same catalog offline (that is the whole point of the grounded layer), so
|
|
1004
|
+
* shipping it would be payload nothing reads.
|
|
1005
|
+
*/
|
|
1006
|
+
interface ExplainIssueGrounding {
|
|
1007
|
+
/**
|
|
1008
|
+
* The id of the matched catalog/platform solution the prompt was grounded in,
|
|
1009
|
+
* absent when nothing recognized the message. The client renders a caveat on
|
|
1010
|
+
* absence — an ungrounded explanation is a free-form model guess, not a
|
|
1011
|
+
* catalog-backed one, and must not be presented as the latter.
|
|
1012
|
+
*/
|
|
1013
|
+
groundedId?: string;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* The `__lunora_admin__:explainIssue` result — a discriminated union on `degraded`
|
|
1017
|
+
* rather than a bag of optionals, so each outcome's guaranteed fields are
|
|
1018
|
+
* guaranteed in the type too. The AI `explanation` is best-effort: the degraded
|
|
1019
|
+
* arm is returned when no `env.AI` binding is configured or the inference call
|
|
1020
|
+
* failed, and the client falls back to its own grounded hint alone.
|
|
1021
|
+
*
|
|
1022
|
+
* Modelling this as one flat interface let a `degraded` result type-check without a
|
|
1023
|
+
* `reason`, which the studio silently renders as the generic AI-error copy.
|
|
1024
|
+
*/
|
|
1025
|
+
/** The arm returned when no inference happened, or it failed. */
|
|
1026
|
+
interface ExplainIssueDegraded extends ExplainIssueGrounding {
|
|
1027
|
+
/** The AI path was unavailable or failed — render the grounded hint instead. */
|
|
1028
|
+
degraded: true;
|
|
1029
|
+
/** Why the AI path degraded, for the client to surface. */
|
|
1030
|
+
reason: ExplainIssueDegradedReason;
|
|
1031
|
+
}
|
|
1032
|
+
/** The arm returned when the model ran and produced text. */
|
|
1033
|
+
interface ExplainIssueSuccess extends ExplainIssueGrounding {
|
|
1034
|
+
/** The AI path ran and produced text. */
|
|
1035
|
+
degraded: false;
|
|
1036
|
+
/** The AI-generated plain-language explanation. */
|
|
1037
|
+
explanation: string;
|
|
1038
|
+
/** The Workers AI model-id that produced {@link ExplainIssueSuccess.explanation}. */
|
|
1039
|
+
model: string;
|
|
1040
|
+
}
|
|
1041
|
+
type ExplainIssueResult = ExplainIssueDegraded | ExplainIssueSuccess;
|
|
1042
|
+
/**
|
|
1043
|
+
* Validate the `__lunora_admin__:explainIssue` payload. Requires a non-empty
|
|
1044
|
+
* `sampleMessage` (the grounding fact); `title`, `culprit`, and `model` are
|
|
1045
|
+
* optional context/overrides. Throws a 400 `LunoraError` on a bad shape.
|
|
1046
|
+
*
|
|
1047
|
+
* Every caller-supplied field that reaches the prompt is capped here — capping
|
|
1048
|
+
* `sampleMessage` alone left `title`/`culprit` as an open door onto the same
|
|
1049
|
+
* prompt budget.
|
|
1050
|
+
*/
|
|
1051
|
+
declare const parseExplainIssueArgs: (args: Record<string, unknown>) => ExplainIssueArgs;
|
|
1052
|
+
/**
|
|
1053
|
+
* Run the full explain flow for one Issue: validate the payload, ground it in the
|
|
1054
|
+
* catalog, and — when `binding` is a usable Workers AI binding — ask the model for
|
|
1055
|
+
* a plain-language rewrite. Never throws for an AI-side failure; every such path
|
|
1056
|
+
* returns the `degraded: true` arm carrying the grounded hint, so the caller
|
|
1057
|
+
* always has something to render. Only a malformed payload throws (a 400).
|
|
1058
|
+
*
|
|
1059
|
+
* `binding` is `unknown` so the caller can hand over `env.AI` untyped — the shape
|
|
1060
|
+
* check lives here rather than at each call site.
|
|
1061
|
+
*/
|
|
1062
|
+
declare const explainIssue: (binding: unknown, args: Record<string, unknown>, options?: {
|
|
1063
|
+
defaultModel?: string;
|
|
1064
|
+
}) => Promise<ExplainIssueResult>;
|
|
1065
|
+
/** Reserved table holding one triage-state row per Issue fingerprint. Auto-hidden by the `__lunora` prefix. */
|
|
1066
|
+
declare const ISSUE_STATE_TABLE = "__lunora_issue_state__";
|
|
1067
|
+
/**
|
|
1068
|
+
* Triage status of an Issue. `open` is the implicit default (no state row); a
|
|
1069
|
+
* developer moves it to `resolved` (fixed, but a *new* matching error re-opens
|
|
1070
|
+
* it — see {@link readIssueStates}'s consumer) or `ignored` (deliberately muted,
|
|
1071
|
+
* and stays muted regardless of new occurrences).
|
|
1072
|
+
*/
|
|
1073
|
+
type IssueStatus = "ignored" | "open" | "resolved";
|
|
1074
|
+
/** Ordered severity a developer can tag an Issue with; drives the Studio badge palette. */
|
|
1075
|
+
type IssueSeverity = "critical" | "high" | "low" | "medium";
|
|
1076
|
+
/** The persisted triage state for one Issue fingerprint. */
|
|
1077
|
+
interface IssueState {
|
|
1078
|
+
/** Free-form assignee (a userId or a name); absent when unassigned. */
|
|
1079
|
+
assignee?: string;
|
|
1080
|
+
/** Stable 16-char fingerprint hash — the same key `readErrorIssues` folds on. */
|
|
1081
|
+
hash: string;
|
|
1082
|
+
/** Developer-tagged severity; absent when untriaged. */
|
|
1083
|
+
severity?: IssueSeverity;
|
|
1084
|
+
/** Current triage status. */
|
|
1085
|
+
status: IssueStatus;
|
|
1086
|
+
/** Wall-clock millis the state was last changed — compared against an Issue's `lastSeen` to detect a regression. */
|
|
1087
|
+
updatedAt: number;
|
|
1088
|
+
/** Acting userId that last changed the state, when known. */
|
|
1089
|
+
updatedBy?: string;
|
|
1090
|
+
}
|
|
1091
|
+
/** Patch applied by an admin write; every field is optional so a caller can change one facet at a time. */
|
|
1092
|
+
interface IssueStatePatch {
|
|
1093
|
+
assignee?: null | string;
|
|
1094
|
+
severity?: IssueSeverity | null;
|
|
1095
|
+
status?: IssueStatus;
|
|
1096
|
+
}
|
|
1097
|
+
/** The valid {@link IssueStatus} values, for arg validation at the admin boundary. */
|
|
1098
|
+
declare const ISSUE_STATUSES: ReadonlyArray<IssueStatus>;
|
|
1099
|
+
/** The valid {@link IssueSeverity} values, for arg validation at the admin boundary. */
|
|
1100
|
+
declare const ISSUE_SEVERITIES: ReadonlyArray<IssueSeverity>;
|
|
1101
|
+
/**
|
|
1102
|
+
* Apply a triage patch to one Issue, upserting its state row. A `null` in the
|
|
1103
|
+
* patch clears the field (unassign, untag severity); an omitted field is left
|
|
1104
|
+
* unchanged. Returns the resulting {@link IssueState} so the caller can echo it.
|
|
1105
|
+
*
|
|
1106
|
+
* Uses `ON CONFLICT(hash) DO UPDATE` with `COALESCE(?, column)` per optional
|
|
1107
|
+
* field so a partial patch touches only what it names — except the explicit
|
|
1108
|
+
* `null` sentinels for assignee/severity, which are threaded separately so a
|
|
1109
|
+
* clear can win over `COALESCE`.
|
|
1110
|
+
*/
|
|
1111
|
+
declare const upsertIssueState: (sql: SqlExec, hash: string, patch: IssueStatePatch, updatedAt: number, updatedBy?: string) => IssueState;
|
|
1112
|
+
/**
|
|
1113
|
+
* Severity of a buffered log entry — the full seven-tier `ctx.log` ramp
|
|
1114
|
+
* (`trace`→`fatal`), not a console-shaped subset. The buffer used to fold the
|
|
1115
|
+
* ramp onto four tiers, which made `trace` and `fatal` lines indistinguishable
|
|
1116
|
+
* from `debug` and `error` in the Studio Logs panel; it now stores the level the
|
|
1117
|
+
* caller actually logged at. Container-lifecycle entries only ever use
|
|
1118
|
+
* `info`/`error`, which remain part of the union.
|
|
1119
|
+
*/
|
|
1120
|
+
type LogLevel = ContextLogLevel;
|
|
1121
|
+
/**
|
|
1122
|
+
* One buffered log line. `functionPath` is the RPC that produced it (when the
|
|
1123
|
+
* entry came from the RPC dispatch site); `timestamp` is `Date.now()` at the
|
|
1124
|
+
* moment it was pushed. `instance`/`exitCode` are populated for container
|
|
1125
|
+
* lifecycle entries: `instance` correlates the per-instance Durable Object id,
|
|
1126
|
+
* `exitCode` carries the process exit code parsed out of a `stop` event.
|
|
1127
|
+
*/
|
|
1128
|
+
interface LogEntry {
|
|
1129
|
+
exitCode?: number;
|
|
1130
|
+
/** Structured fields from a `ctx.log.<level>(message, fields)` / `ctx.log.with(fields)` call, when present. */
|
|
1131
|
+
fields?: Record<string, unknown>;
|
|
1132
|
+
functionPath?: string;
|
|
1133
|
+
instance?: string;
|
|
1134
|
+
level: LogLevel;
|
|
1135
|
+
message: string;
|
|
1136
|
+
timestamp: number;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* A bounded, in-memory ring buffer of recent {@link LogEntry} records.
|
|
1140
|
+
*
|
|
1141
|
+
* In-memory only: like the metrics counters on `ShardDO`, the buffer is a field
|
|
1142
|
+
* on the live Durable Object instance and so resets whenever the DO hibernates
|
|
1143
|
+
* or restarts. It is a "recent activity on this instance" readout (for the
|
|
1144
|
+
* studio's live log panel), NOT a durable log store or a transport.
|
|
1145
|
+
* Production log shipping is the platform's job, not ours: use Cloudflare
|
|
1146
|
+
* **Workers Logs** (retained, queryable in the studio), **Logpush** (stream
|
|
1147
|
+
* to R2 / a SIEM / a log service), or a **Tail Worker** for programmatic
|
|
1148
|
+
* capture. Lunora deliberately does not reimplement any of those — this buffer
|
|
1149
|
+
* stays a tiny dev/ops readout. Capacity is fixed at construction; once full,
|
|
1150
|
+
* the oldest entry is evicted to make room (FIFO), so memory stays bounded
|
|
1151
|
+
* regardless of traffic.
|
|
1152
|
+
*/
|
|
1153
|
+
declare class LogBuffer {
|
|
1154
|
+
/** Backing store, kept in insertion order (oldest first). */
|
|
1155
|
+
private readonly buffer;
|
|
1156
|
+
private readonly capacity;
|
|
1157
|
+
constructor(capacity?: number);
|
|
1158
|
+
/** Number of entries currently buffered. */
|
|
1159
|
+
get size(): number;
|
|
1160
|
+
/** Drop every buffered entry. */
|
|
1161
|
+
clear(): void;
|
|
1162
|
+
/**
|
|
1163
|
+
* Snapshot of the buffered entries, **newest first** so the panel renders
|
|
1164
|
+
* the most recent activity at the top without re-sorting. Returns a fresh
|
|
1165
|
+
* array each call; the caller may mutate it freely.
|
|
1166
|
+
*/
|
|
1167
|
+
entries(): LogEntry[];
|
|
1168
|
+
/**
|
|
1169
|
+
* Append an entry, evicting the oldest when at capacity so the buffer never
|
|
1170
|
+
* grows past its bound.
|
|
1171
|
+
*/
|
|
1172
|
+
push(entry: LogEntry): void;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* One aggregated metric series: every measurement sharing a `(name, kind,
|
|
1176
|
+
* attributes)` identity folded into a single running summary.
|
|
1177
|
+
*
|
|
1178
|
+
* All fields are maintained for every {@link MetricKind} — the fold is uniform
|
|
1179
|
+
* and O(1), and letting the panel choose the meaningful projection per kind
|
|
1180
|
+
* (counter → `sum`, gauge → `last`, histogram → `sum`/`count` for the mean, plus
|
|
1181
|
+
* `min`/`max`) is cheaper and clearer than branching on kind at record time.
|
|
1182
|
+
*/
|
|
1183
|
+
interface MetricSeries {
|
|
1184
|
+
/** The series' dimensions, if any — the attributes that made it distinct. */
|
|
1185
|
+
attributes?: LogFields;
|
|
1186
|
+
/** Number of measurements folded into this series. */
|
|
1187
|
+
count: number;
|
|
1188
|
+
/** Trace id of the most recent measurement that carried one — the series' exemplar, for linking to a trace. */
|
|
1189
|
+
exemplarTraceId?: string;
|
|
1190
|
+
/** Wall-clock millis of the first measurement folded in. */
|
|
1191
|
+
firstTs: number;
|
|
1192
|
+
/** Function path that recorded the series' most recent measurement. */
|
|
1193
|
+
functionPath: string;
|
|
1194
|
+
/** Instrument kind; decides which projection the panel shows. */
|
|
1195
|
+
kind: MetricKind;
|
|
1196
|
+
/** Most recent measured value — the current reading for a `gauge`. */
|
|
1197
|
+
last: number;
|
|
1198
|
+
/** Wall-clock millis of the most recent measurement. */
|
|
1199
|
+
lastTs: number;
|
|
1200
|
+
/** Largest measured value seen. */
|
|
1201
|
+
max: number;
|
|
1202
|
+
/** Smallest measured value seen. */
|
|
1203
|
+
min: number;
|
|
1204
|
+
/** Instrument name, e.g. `"orders.placed"`. */
|
|
1205
|
+
name: string;
|
|
1206
|
+
/** Shard key for single-shard calls; absent for the unnamed root DO. */
|
|
1207
|
+
shardKey?: string;
|
|
1208
|
+
/** Sum of measured values — a `counter`'s total, a `histogram`'s sum. */
|
|
1209
|
+
sum: number;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* A bounded map of running metric aggregates, keyed by series identity. Eviction
|
|
1213
|
+
* is least-recently-*updated*: a re-recorded series moves back to the tail (Map
|
|
1214
|
+
* insertion order), so the capacity bound sheds cold, high-cardinality series and
|
|
1215
|
+
* keeps the ones still receiving traffic — the opposite of a raw ring, which
|
|
1216
|
+
* would evict a hot counter's own history.
|
|
1217
|
+
*/
|
|
1218
|
+
declare class MetricBuffer {
|
|
1219
|
+
private readonly capacity;
|
|
1220
|
+
private readonly series;
|
|
1221
|
+
constructor(capacity?: number);
|
|
1222
|
+
/** Number of distinct series currently aggregated. */
|
|
1223
|
+
get size(): number;
|
|
1224
|
+
/** Drop every aggregated series. */
|
|
1225
|
+
clear(): void;
|
|
1226
|
+
/**
|
|
1227
|
+
* Snapshot of the aggregated series, most-recently-updated first, each a fresh
|
|
1228
|
+
* copy so a caller can't mutate the live aggregate. `series` is kept in
|
|
1229
|
+
* update order (tail = newest), so one reverse yields newest-first.
|
|
1230
|
+
*/
|
|
1231
|
+
entries(): MetricSeries[];
|
|
1232
|
+
/** Fold one measurement into its series, creating or updating the aggregate. */
|
|
1233
|
+
push(event: MetricEvent): void;
|
|
1234
|
+
}
|
|
1235
|
+
/** One time-bucket sample of a series: the aggregate over `[bucketMs, bucketMs + METRIC_HISTORY_BUCKET_MS)`. */
|
|
1236
|
+
interface MetricHistoryPoint {
|
|
1237
|
+
/** Epoch-ms floor of the bucket window. */
|
|
1238
|
+
bucketMs: number;
|
|
1239
|
+
/** Measurements folded into this bucket. */
|
|
1240
|
+
count: number;
|
|
1241
|
+
/** Sample `traceId` of a measurement in this bucket, if one carried trace context — the exemplar. */
|
|
1242
|
+
exemplarTraceId?: string;
|
|
1243
|
+
/** Last measured value in the bucket — a gauge's reading at the window's end. */
|
|
1244
|
+
last: number;
|
|
1245
|
+
/** Largest value in the bucket. */
|
|
1246
|
+
max: number;
|
|
1247
|
+
/** Smallest value in the bucket. */
|
|
1248
|
+
min: number;
|
|
1249
|
+
/** Sum of values in the bucket — a counter's increment total, a histogram's sum. */
|
|
1250
|
+
sum: number;
|
|
1251
|
+
}
|
|
1252
|
+
/** One series' durable history: its identity plus its time-ordered buckets, oldest first. */
|
|
1253
|
+
interface MetricHistorySeries {
|
|
1254
|
+
attributes?: LogFields;
|
|
1255
|
+
functionPath: string;
|
|
1256
|
+
kind: MetricKind;
|
|
1257
|
+
name: string;
|
|
1258
|
+
/** Buckets in ascending `bucketMs` order, ready to chart as a line. */
|
|
1259
|
+
points: MetricHistoryPoint[];
|
|
1260
|
+
shardKey?: string;
|
|
1261
|
+
}
|
|
1262
|
+
/** {@link readMetricHistory} result: every tracked series with its buckets. */
|
|
1263
|
+
interface MetricHistoryResult {
|
|
1264
|
+
series: MetricHistorySeries[];
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Tunable caps for {@link recordMetricHistory}, threaded from the sink's
|
|
1268
|
+
* `metricHistory` option. Each falls back to its module-constant default, so an
|
|
1269
|
+
* omitted field keeps the historical behaviour.
|
|
1270
|
+
*/
|
|
1271
|
+
interface MetricHistoryOptions {
|
|
1272
|
+
/** Distinct series tracked before a brand-new one is dropped (default {@link METRIC_HISTORY_MAX_SERIES}). */
|
|
1273
|
+
maxSeries?: number;
|
|
1274
|
+
/** Minute-buckets kept per series before older rows are trimmed (default {@link METRIC_HISTORY_BUCKET_RETENTION}). */
|
|
1275
|
+
retentionBuckets?: number;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Fold one measurement into its `(series, minute)` bucket. Runs per
|
|
1279
|
+
* `ctx.metrics.*` call (unlike `function-metrics.ts`, which runs once per
|
|
1280
|
+
* dispatch), so it's tuned for the in-minute repeat: a bucket this instance has
|
|
1281
|
+
* already written is a single upsert (no reads), a bucket only in the DB costs one
|
|
1282
|
+
* PK point-lookup + upsert, and only a genuinely new bucket also pays the
|
|
1283
|
+
* distinct-series cap scan + retention trim. Still a durable SQLite write per
|
|
1284
|
+
* measurement, so a hot loop recording thousands of points a second should
|
|
1285
|
+
* pre-aggregate and record once (see `shared/metric-event.ts`). Creates the table
|
|
1286
|
+
* first so callers needn't.
|
|
1287
|
+
*
|
|
1288
|
+
* `exemplarTraceId` (the recording dispatch's trace, when it had one) is stored on
|
|
1289
|
+
* the bucket so the studio can link a chart point back to a trace. Latest wins:
|
|
1290
|
+
* a later sample carrying a trace replaces an earlier bucket's exemplar.
|
|
1291
|
+
*
|
|
1292
|
+
* `options` tunes the distinct-series cap and retention window from the sink's
|
|
1293
|
+
* `metricHistory` flag (see {@link MetricHistoryOptions}); each defaults to its
|
|
1294
|
+
* module constant.
|
|
1295
|
+
*/
|
|
1296
|
+
declare const recordMetricHistory: (sql: SqlExec, event: MetricEvent, exemplarTraceId?: string, options?: MetricHistoryOptions) => void;
|
|
1297
|
+
/**
|
|
1298
|
+
* Read the durable history, grouped into one {@link MetricHistorySeries} per
|
|
1299
|
+
* series with its buckets in ascending time order.
|
|
1300
|
+
*
|
|
1301
|
+
* Rows are fetched most-recent-first (`bucket_ms DESC`) under the row cap, NOT
|
|
1302
|
+
* `series_key`-ordered: all active series write the same recent minutes, so this
|
|
1303
|
+
* windows every series to a recent slice fairly, instead of handing the
|
|
1304
|
+
* alphabetically-first series its full 1440-bucket history and starving the rest
|
|
1305
|
+
* once the cap is hit. Each series' points are re-sorted ascending below, since a
|
|
1306
|
+
* trend line reads oldest→newest.
|
|
1307
|
+
*
|
|
1308
|
+
* `options.sinceMs`, when set, returns only buckets at or after this epoch-ms —
|
|
1309
|
+
* the studio's time-window selector.
|
|
1310
|
+
*/
|
|
1311
|
+
declare const readMetricHistory: (sql: SqlExec, options?: {
|
|
1312
|
+
sinceMs?: number;
|
|
1313
|
+
}) => MetricHistoryResult;
|
|
1314
|
+
/** One row of the `__lunora_metrics_queries` table, as returned by `readQueryMetrics`. */
|
|
1315
|
+
interface QueryStatEntry {
|
|
1316
|
+
/** Total number of times this statement was executed. */
|
|
1317
|
+
execCount: number;
|
|
1318
|
+
/** Normalised SQL text (literals stripped, truncated). */
|
|
1319
|
+
normalizedSql: string;
|
|
1320
|
+
/** Total rows read across all executions (SELECT result sizes). */
|
|
1321
|
+
rowsRead: number;
|
|
1322
|
+
/** Total rows written across all executions. */
|
|
1323
|
+
rowsWritten: number;
|
|
1324
|
+
/** Total wall-clock milliseconds across all executions. */
|
|
1325
|
+
totalDurationMs: number;
|
|
1326
|
+
}
|
|
1327
|
+
/** One statement's activity within a chosen time range. */
|
|
1328
|
+
interface QueryInsightEntry {
|
|
1329
|
+
/** Mean milliseconds per execution across the range. */
|
|
1330
|
+
avgDurationMs: number;
|
|
1331
|
+
execCount: number;
|
|
1332
|
+
normalizedSql: string;
|
|
1333
|
+
/** Interpolated 50th percentile latency, in milliseconds. */
|
|
1334
|
+
p50DurationMs: number;
|
|
1335
|
+
/** Interpolated 95th percentile latency, in milliseconds. */
|
|
1336
|
+
p95DurationMs: number;
|
|
1337
|
+
rowsRead: number;
|
|
1338
|
+
rowsWritten: number;
|
|
1339
|
+
totalDurationMs: number;
|
|
1340
|
+
}
|
|
1341
|
+
/** One point on the throughput/latency charts. */
|
|
1342
|
+
interface QueryInsightBucket {
|
|
1343
|
+
/** Mean milliseconds per execution in this window, across all statements. */
|
|
1344
|
+
avgDurationMs: number;
|
|
1345
|
+
/** Bucket start, epoch millis. */
|
|
1346
|
+
bucketMs: number;
|
|
1347
|
+
execCount: number;
|
|
1348
|
+
}
|
|
1349
|
+
/** What `getQueryInsights` returns. */
|
|
1350
|
+
interface QueryInsightsResult {
|
|
1351
|
+
/** Time series across the whole range, all statements combined. */
|
|
1352
|
+
buckets: QueryInsightBucket[];
|
|
1353
|
+
/**
|
|
1354
|
+
* True when the tracked-statement cap has been reached, so the caller can say
|
|
1355
|
+
* "showing N of a capped set" rather than implying totality. Silent
|
|
1356
|
+
* truncation reads as complete coverage when it is not.
|
|
1357
|
+
*/
|
|
1358
|
+
capped: boolean;
|
|
1359
|
+
entries: QueryInsightEntry[];
|
|
1360
|
+
/** How many distinct statements the lifetime table is tracking. */
|
|
1361
|
+
trackedStatements: number;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Per-statement activity within `rangeMs` of `now`, plus a combined time series.
|
|
1365
|
+
*
|
|
1366
|
+
* Reads the bucket table (not the lifetime one) so the numbers answer "what is
|
|
1367
|
+
* hot right now"; the statement TEXT is joined back from the lifetime table,
|
|
1368
|
+
* which is the only place it is stored.
|
|
1369
|
+
*/
|
|
1370
|
+
declare const readQueryInsights: (sql: SqlExec, rangeMs: number, now?: number) => QueryInsightsResult;
|
|
1371
|
+
/**
|
|
1372
|
+
* Record one statement execution. Creates the table on first call. Silently
|
|
1373
|
+
* skips recording when the normalised statement is empty (shouldn't happen
|
|
1374
|
+
* in practice) or when the table is already at the
|
|
1375
|
+
* {@link QUERY_METRICS_MAX_STATEMENTS} cap and the statement is not yet
|
|
1376
|
+
* tracked. The cap check is a single cheap `COUNT(*)` on the primary-key
|
|
1377
|
+
* index, so the hot-path cost is minimal.
|
|
1378
|
+
*/
|
|
1379
|
+
declare const recordQueryMetric: (sql: SqlExec, rawSql: string, durationMs: number, rowsRead: number, rowsWritten: number, now?: number) => void;
|
|
1380
|
+
/**
|
|
1381
|
+
* Read all tracked statement aggregates, ordered by `total_duration_ms DESC`
|
|
1382
|
+
* (the leaderboard's default). Creates the table first so a read on a
|
|
1383
|
+
* never-called shard returns `[]`.
|
|
1384
|
+
*/
|
|
1385
|
+
declare const readQueryMetrics: (sql: SqlExec) => QueryStatEntry[];
|
|
1386
|
+
/** Reserved append-only table backing the studio Logs tab. Auto-hidden from the data browser by the `__lunora` prefix. */
|
|
1387
|
+
declare const REQUEST_LOG_TABLE = "__lunora_reqlog__";
|
|
1388
|
+
/** Outcome of one dispatch — `ok` for a returned result, `error` for a thrown handler. */
|
|
1389
|
+
type RequestOutcome = "error" | "ok";
|
|
1390
|
+
/** One recorded `/rpc` dispatch, in monotonic `seq` order. */
|
|
1391
|
+
interface RequestLogEntry {
|
|
1392
|
+
/** Whether the result was served from the reactive cache; `undefined` when the cache is disabled or the path isn't cached (a write/action). */
|
|
1393
|
+
cacheHit?: boolean;
|
|
1394
|
+
/** Handler wall-clock duration in milliseconds (before the subscription write-flush, matching the per-function metrics). */
|
|
1395
|
+
durationMs: number;
|
|
1396
|
+
/** Error message when `outcome === "error"`; absent on success. */
|
|
1397
|
+
errorMessage?: string;
|
|
1398
|
+
/** The `<file>:<function>` identifier dispatched, e.g. `messages:list`. */
|
|
1399
|
+
functionPath: string;
|
|
1400
|
+
/** 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. */
|
|
1401
|
+
identity?: Record<string, unknown>;
|
|
1402
|
+
/** `ok` for a returned result, `error` for a thrown handler. */
|
|
1403
|
+
outcome: RequestOutcome;
|
|
1404
|
+
/** Call args with leaf values redacted by default (keys/shape preserved); absent when no args were sent. */
|
|
1405
|
+
redactedArgs?: unknown;
|
|
1406
|
+
/** Monotonic per-shard cursor — strictly increasing, never reused. */
|
|
1407
|
+
seq: number;
|
|
1408
|
+
/** Shard key (the DO id name), or `undefined` for the unnamed `__root__` DO. */
|
|
1409
|
+
shardKey?: string;
|
|
1410
|
+
/** Count of subscriptions re-run by the write this dispatch triggered; `0` when none (or not measured at the dispatch site). */
|
|
1411
|
+
subscriptionsReRun: number;
|
|
1412
|
+
/** Tables the handler read (from the dependency tracker); empty when the reactive cache is off or the path read nothing. */
|
|
1413
|
+
tablesRead: string[];
|
|
1414
|
+
/** Tables the handler wrote (from the change tracker); empty for a read-only dispatch. */
|
|
1415
|
+
tablesWritten: string[];
|
|
1416
|
+
/** Wall-clock millis when the dispatch completed. */
|
|
1417
|
+
ts: number;
|
|
1418
|
+
/** Acting userId forwarded by the runtime, or `undefined` when anonymous. */
|
|
1419
|
+
userId?: string;
|
|
1420
|
+
}
|
|
1421
|
+
/** Fields accepted when appending one request-log entry; `seq` is assigned by the table. */
|
|
1422
|
+
interface AppendRequestLogEntry {
|
|
1423
|
+
cacheHit?: boolean;
|
|
1424
|
+
durationMs: number;
|
|
1425
|
+
errorMessage?: string;
|
|
1426
|
+
functionPath: string;
|
|
1427
|
+
identity?: Record<string, unknown>;
|
|
1428
|
+
outcome: RequestOutcome;
|
|
1429
|
+
redactedArgs?: unknown;
|
|
1430
|
+
shardKey?: string;
|
|
1431
|
+
subscriptionsReRun?: number;
|
|
1432
|
+
tablesRead?: string[];
|
|
1433
|
+
tablesWritten?: string[];
|
|
1434
|
+
ts: number;
|
|
1435
|
+
userId?: string;
|
|
1436
|
+
}
|
|
1437
|
+
/** Knobs the dispatch site threads into a request-log write. */
|
|
1438
|
+
interface RequestLogWriteOptions {
|
|
1439
|
+
/** When `true` (development only), skip args/identity redaction so a developer sees raw values. Defaults to `false` (production-safe). */
|
|
1440
|
+
captureRaw?: boolean;
|
|
1441
|
+
/** Rows to keep after the append-time trim; defaults to {@link REQUEST_LOG_RETENTION}. The operator's `LUNORA_REQUEST_LOG_RETENTION` override. */
|
|
1442
|
+
retention?: number;
|
|
1443
|
+
}
|
|
1444
|
+
/** Filters for {@link readRequestLog}, all AND-combined; every value is a bound SQL parameter, so nothing here injects SQL. */
|
|
1445
|
+
interface ReadRequestLogOptions {
|
|
1446
|
+
/** Functions whose path begins with this prefix (a `<file>:` or `<file>:<fn>` correlation). */
|
|
1447
|
+
functionPathPrefix?: string;
|
|
1448
|
+
/** Upper bound on returned rows, clamped to [1, 10000]. */
|
|
1449
|
+
limit?: number;
|
|
1450
|
+
/** Keep only `ok` / `error` outcomes. */
|
|
1451
|
+
outcome?: RequestOutcome;
|
|
1452
|
+
/** Exact shard-key match. */
|
|
1453
|
+
shardKey?: string;
|
|
1454
|
+
/** Only entries strictly after this cursor (forward paging). */
|
|
1455
|
+
sinceSeq?: number;
|
|
1456
|
+
/** Keep only entries whose read OR written table set contains this table. */
|
|
1457
|
+
tableTouched?: string;
|
|
1458
|
+
/** Exact acting-userId match. */
|
|
1459
|
+
userId?: string;
|
|
1460
|
+
}
|
|
1461
|
+
/** Payload of a `__lunora_admin__:getRequestLog` call: the recorded entries, newest first. */
|
|
1462
|
+
interface RequestLogResult {
|
|
1463
|
+
entries: RequestLogEntry[];
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* One grouped error **Issue**: many `error`-outcome request-log rows that share a
|
|
1467
|
+
* fingerprint folded into a single triage row. The `hash` is the same stable key
|
|
1468
|
+
* a cloud Incident groups on, so a local Issue and a cloud Incident are the same
|
|
1469
|
+
* object.
|
|
1470
|
+
*/
|
|
1471
|
+
interface ErrorIssue {
|
|
1472
|
+
/** Assignee (a userId or a name) from the persisted triage state; absent when unassigned. */
|
|
1473
|
+
assignee?: string;
|
|
1474
|
+
/** Number of `error` rows folded into this Issue within the scanned window. */
|
|
1475
|
+
count: number;
|
|
1476
|
+
/** The `<file>:<function>` (or `container:<name>`) the errors came from. */
|
|
1477
|
+
culprit: string;
|
|
1478
|
+
/** Wall-clock millis of the oldest folded row. */
|
|
1479
|
+
firstSeen: number;
|
|
1480
|
+
/** Stable 16-char grouping hash over `functionPath :: bucket(message)`. */
|
|
1481
|
+
hash: string;
|
|
1482
|
+
/** Wall-clock millis of the newest folded row. */
|
|
1483
|
+
lastSeen: number;
|
|
1484
|
+
/** A representative raw error message — taken from the most recent folded row. */
|
|
1485
|
+
sampleMessage: string;
|
|
1486
|
+
/** Developer-tagged severity from the persisted triage state; absent when untriaged. */
|
|
1487
|
+
severity?: IssueSeverity;
|
|
1488
|
+
/**
|
|
1489
|
+
* Wall-clock millis the persisted triage state was last changed; absent when
|
|
1490
|
+
* the Issue has never been triaged. Compared against `lastSeen` to detect a
|
|
1491
|
+
* regression (a new error after a resolve).
|
|
1492
|
+
*/
|
|
1493
|
+
stateUpdatedAt?: number;
|
|
1494
|
+
/**
|
|
1495
|
+
* Triage status folded in from the persisted state (`open` by default). A
|
|
1496
|
+
* `resolved` Issue whose `lastSeen` is newer than `stateUpdatedAt` is
|
|
1497
|
+
* auto-reopened to `open` here (a regression), so a fresh occurrence never
|
|
1498
|
+
* hides behind a stale resolution; `ignored` stays sticky by design.
|
|
1499
|
+
*/
|
|
1500
|
+
status: IssueStatus;
|
|
1501
|
+
/** Human-readable title (first line of the sample message, capped). */
|
|
1502
|
+
title: string;
|
|
1503
|
+
}
|
|
1504
|
+
/** Payload of a `__lunora_admin__:getIssues` call: grouped error Issues, most-recently-active first. */
|
|
1505
|
+
interface IssuesResult {
|
|
1506
|
+
issues: ErrorIssue[];
|
|
1507
|
+
}
|
|
1508
|
+
/** Filters for {@link readErrorIssues}; forwarded to {@link readRequestLog} with `outcome` forced to `error`. */
|
|
1509
|
+
interface ReadIssuesOptions {
|
|
1510
|
+
/** Functions whose path begins with this prefix (a `<file>:` or `<file>:<fn>` correlation). */
|
|
1511
|
+
functionPathPrefix?: string;
|
|
1512
|
+
/** Upper bound on error rows scanned before grouping, clamped to [1, 10000]. */
|
|
1513
|
+
limit?: number;
|
|
1514
|
+
/** Exact shard-key match. */
|
|
1515
|
+
shardKey?: string;
|
|
1516
|
+
/** Keep only Issues in this triage status, applied AFTER the persisted-state fold + auto-reopen. */
|
|
1517
|
+
status?: IssueStatus;
|
|
1518
|
+
/** Exact acting-userId match. */
|
|
1519
|
+
userId?: string;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Create the `__lunora_reqlog__` table. `seq` is an `AUTOINCREMENT` primary
|
|
1523
|
+
* key, giving each shard a monotonic cursor the Logs tab pages through; the
|
|
1524
|
+
* `args`/`identity`/`tables_read`/`tables_written` columns hold JSON and are
|
|
1525
|
+
* `NULL`/empty when none was recorded. Idempotent, so read and write paths can
|
|
1526
|
+
* call it defensively.
|
|
1527
|
+
*/
|
|
1528
|
+
declare const ensureRequestLogTable: (sql: SqlExec) => void;
|
|
1529
|
+
/**
|
|
1530
|
+
* Append one dispatch to the request log, then trim the log back to the most
|
|
1531
|
+
* recent `retention` rows (default {@link REQUEST_LOG_RETENTION}). Creates the
|
|
1532
|
+
* table first so callers needn't. Args/identity are redacted here so a raw value
|
|
1533
|
+
* never reaches the durable table — callers pass the unredacted entry and rely on
|
|
1534
|
+
* this, unless `captureRaw` (dev only) is set. `retention` is the operator's
|
|
1535
|
+
* `LUNORA_REQUEST_LOG_RETENTION` override, threaded in by the dispatch site.
|
|
1536
|
+
*/
|
|
1537
|
+
declare const appendRequestLogEntry: (sql: SqlExec, entry: AppendRequestLogEntry, options?: RequestLogWriteOptions) => void;
|
|
1538
|
+
/**
|
|
1539
|
+
* Emit one structured request event to `console` so Cloudflare's Workers Logs /
|
|
1540
|
+
* Logpush pipeline carries it to external sinks (SIEMs) — PLAN3 §3.3. This does
|
|
1541
|
+
* NOT reimplement a transport: it produces a richer, lunora-attributed event and
|
|
1542
|
+
* lets CF's existing trace-log pipe ship it. The event mirrors the durable
|
|
1543
|
+
* `__lunora_reqlog__` row (function path, shard, user, outcome, duration, tables
|
|
1544
|
+
* read/written, cache hit), with `args` AND `identity` redacted exactly like the
|
|
1545
|
+
* durable write so no raw PII/secret reaches the log pipeline.
|
|
1546
|
+
*
|
|
1547
|
+
* An `error` outcome goes to `console.error` (surfacing at error level in the
|
|
1548
|
+
* trace so a SIEM can alert on it); everything else to `console.log`. The
|
|
1549
|
+
* `source: "lunora"` / `type: "request"` envelope lets a consumer filter these
|
|
1550
|
+
* events out of the raw Workers-trace firehose. `captureRaw` (dev only) skips
|
|
1551
|
+
* redaction, mirroring the durable write. Best-effort by contract — the caller
|
|
1552
|
+
* wraps it so a serialization hiccup can never fail the served request.
|
|
1553
|
+
*/
|
|
1554
|
+
declare const emitRequestLogEvent: (entry: AppendRequestLogEntry, options?: RequestLogWriteOptions) => void;
|
|
1555
|
+
/**
|
|
1556
|
+
* The `ctx.log` event contract (shape + severity union) lives in
|
|
1557
|
+
* `shared/log-event.ts` (inlined into each `dist`) so the DO that builds these
|
|
1558
|
+
* events and the `@lunora/runtime` sink that consumes them agree by construction.
|
|
1559
|
+
* `LogEventInput` is the DO's historical name for the shared `LogEvent`.
|
|
1560
|
+
*/
|
|
1561
|
+
type LogEventInput = LogEvent;
|
|
1562
|
+
/**
|
|
1563
|
+
* Split a `ctx.log.<level>(...)` call's raw arguments into a display `message`
|
|
1564
|
+
* and optional structured `fields`. The structured form — a message string plus
|
|
1565
|
+
* a plain-object fields bag — is matched only for exactly `(string, object)`;
|
|
1566
|
+
* every other shape is console-style and rendered whole (so existing
|
|
1567
|
+
* `console`-shaped calls are unchanged). Bound `.with(...)` fields merge under
|
|
1568
|
+
* the per-call fields (per-call wins); the result is normalized to a fresh bag
|
|
1569
|
+
* of JSON-safe primitives, or `undefined` when empty (see `normalizeLogFields`).
|
|
1570
|
+
*/
|
|
1571
|
+
declare const parseLogArgs: (args: unknown[], boundFields?: LogFields) => {
|
|
1572
|
+
fields?: LogFields;
|
|
1573
|
+
message: string;
|
|
1574
|
+
};
|
|
1575
|
+
/**
|
|
1576
|
+
* Emit one application-log event from a `ctx.log.*` call to `console`, tagged
|
|
1577
|
+
* `{ source: "lunora", type: "log" }` so the CLI / Vite formatter can pretty-print
|
|
1578
|
+
* it in the dev terminal and a Logpush/SIEM consumer can filter it out of the
|
|
1579
|
+
* raw Workers-trace firehose.
|
|
1580
|
+
*
|
|
1581
|
+
* Only the rendered `message` is emitted here, NOT the structured `args` array:
|
|
1582
|
+
* the console event rides CF Workers Logs / Logpush to prod, and shipping raw,
|
|
1583
|
+
* un-redacted arg objects on a `source: "lunora"` line a SIEM is told to trust
|
|
1584
|
+
* would be a surprising PII/secret surface. The raw `args` stay on the in-process
|
|
1585
|
+
* `sink.onLog` path (`recordUserLog`), which the operator opts into and controls.
|
|
1586
|
+
* `message` already carries the developer's rendered values, exactly like a raw
|
|
1587
|
+
* `console.log` line.
|
|
1588
|
+
*
|
|
1589
|
+
* `error`/`fatal` go to `console.error`, `warn` to `console.warn` (so they
|
|
1590
|
+
* surface at the right level in the trace); every other level to `console.log`.
|
|
1591
|
+
*
|
|
1592
|
+
* Structured `fields` (plus `traceId`/`spanId` for correlation) ARE emitted here
|
|
1593
|
+
* — they are intentional metadata a log pipeline filters on, unlike raw `args`.
|
|
1594
|
+
* A field value that can't be serialised (a circular object) would make
|
|
1595
|
+
* `JSON.stringify` throw and drop the whole line, so serialisation falls back to
|
|
1596
|
+
* a fields-free line rather than losing the event.
|
|
1597
|
+
*/
|
|
1598
|
+
declare const emitLogEvent: (input: LogEventInput) => void;
|
|
1599
|
+
/**
|
|
1600
|
+
* Read request-log entries newest-first, AND-combining the supplied filters
|
|
1601
|
+
* (function-path prefix, exact userId/shardKey/outcome, and a table-touched
|
|
1602
|
+
* match against the read OR written table sets), up to `limit` (clamped to
|
|
1603
|
+
* [1, 10000]). Each value is a bound parameter, so no filter can inject SQL.
|
|
1604
|
+
* Creates the table first so reads on a never-logged shard return `[]` instead
|
|
1605
|
+
* of throwing. Mirrors `readAuditLog`/`readCdcChanges`.
|
|
1606
|
+
*/
|
|
1607
|
+
declare const readRequestLog: (sql: SqlExec, options?: ReadRequestLogOptions) => RequestLogEntry[];
|
|
1608
|
+
declare const readErrorIssues: (sql: SqlExec, options?: ReadIssuesOptions) => ErrorIssue[];
|
|
1609
|
+
/**
|
|
1610
|
+
* Ordering/visual weight of a security finding — mirrors the studio's insight
|
|
1611
|
+
* severities so the Security Advisor and the Performance Advisor (Insights) share
|
|
1612
|
+
* one badge vocabulary. `error` sorts worst-first, then `warning`, then `info`.
|
|
1613
|
+
*/
|
|
1614
|
+
type SecurityFindingLevel = "error" | "info" | "warning";
|
|
1615
|
+
/**
|
|
1616
|
+
* Which security heuristic fired. The detection stays free of presentation
|
|
1617
|
+
* strings — the studio maps each kind to a localized title, explanation, and
|
|
1618
|
+
* remediation hint — so the rule set is trivially unit-testable and the wire
|
|
1619
|
+
* payload is tiny.
|
|
1620
|
+
*
|
|
1621
|
+
* `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.)
|
|
1622
|
+
*
|
|
1623
|
+
* `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.
|
|
1624
|
+
*
|
|
1625
|
+
* `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.
|
|
1626
|
+
*
|
|
1627
|
+
* `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`.
|
|
1628
|
+
*
|
|
1629
|
+
* `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.
|
|
1630
|
+
*
|
|
1631
|
+
* `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.
|
|
1632
|
+
*
|
|
1633
|
+
* `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.
|
|
1634
|
+
*
|
|
1635
|
+
* `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.
|
|
1636
|
+
*/
|
|
1637
|
+
type SecurityFindingKind = "admin-token-weak" | "auth-secret-weak" | "cookies-insecure" | "cors-wildcard-credentials" | "csrf-disabled" | "dev-args-unredacted" | "security-headers-disabled" | "ws-gate-open";
|
|
1638
|
+
/**
|
|
1639
|
+
* One detected security issue. `detail` carries kind-specific context the studio
|
|
1640
|
+
* may interpolate into the localized copy (e.g. the offending token length);
|
|
1641
|
+
* absent when the kind needs none.
|
|
1642
|
+
*/
|
|
1643
|
+
interface SecurityFinding {
|
|
1644
|
+
detail?: Record<string, unknown>;
|
|
1645
|
+
kind: SecurityFindingKind;
|
|
1646
|
+
level: SecurityFindingLevel;
|
|
1647
|
+
}
|
|
1648
|
+
/** Payload of a `__lunora_admin__:getSecurityAudit` call: every detected finding, worst-first. */
|
|
1649
|
+
interface SecurityAuditResult {
|
|
1650
|
+
findings: SecurityFinding[];
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Minimum `LUNORA_ADMIN_TOKEN` length considered safe against brute force. A
|
|
1654
|
+
* short token gates the studio's destructive admin ops (writeRow, clearTable,
|
|
1655
|
+
* pitrRestore, …), so a guessable one is a real exposure. 24 chars ≈ 128 bits
|
|
1656
|
+
* for a random base64-ish token.
|
|
1657
|
+
*/
|
|
1658
|
+
declare const MIN_ADMIN_TOKEN_LENGTH = 24;
|
|
1659
|
+
/**
|
|
1660
|
+
* Minimum `AUTH_SECRET` / `BETTER_AUTH_SECRET` length. better-auth signs session
|
|
1661
|
+
* tokens with this secret; 32 chars (≈ `openssl rand -hex 32` → 32 bytes hex, or
|
|
1662
|
+
* 192 bits of base64) is the floor below which the signing key is brute-forceable.
|
|
1663
|
+
*/
|
|
1664
|
+
declare const MIN_AUTH_SECRET_LENGTH = 32;
|
|
1665
|
+
/**
|
|
1666
|
+
* Audit the Worker `env` for deployment-level security misconfigurations the
|
|
1667
|
+
* Durable Object can observe directly. Pure and side-effect-free — same `env`,
|
|
1668
|
+
* same findings — so the rules unit-test without a live shard.
|
|
1669
|
+
*
|
|
1670
|
+
* This is the server half of the studio's **Security Advisor**: CF's dashboard
|
|
1671
|
+
* is infra-level and can't reason about lunora's admin/WS gates or its
|
|
1672
|
+
* request-log redaction policy, so these are signals only lunora can surface.
|
|
1673
|
+
* The audit is served behind the same admin gate as every other introspection
|
|
1674
|
+
* RPC, so it only runs once a `LUNORA_ADMIN_TOKEN` is configured — which is why
|
|
1675
|
+
* a *missing* token is never itself a finding here (introspection is simply off).
|
|
1676
|
+
*/
|
|
1677
|
+
declare const buildSecurityAudit: (rawEnv: unknown, options: {
|
|
1678
|
+
dev: boolean;
|
|
1679
|
+
}) => SecurityAuditResult;
|
|
1680
|
+
/**
|
|
1681
|
+
* One span in a folded trace, flattened for rendering: `depth` is its nesting
|
|
1682
|
+
* level under the root and `offsetMs` its start relative to the trace start, so
|
|
1683
|
+
* a waterfall row is a pure function of the record (indent by `depth`, bar from
|
|
1684
|
+
* `offsetMs` to `offsetMs + durationMs`) with no client-side tree math.
|
|
1685
|
+
*/
|
|
1686
|
+
interface TraceSpan {
|
|
1687
|
+
attributes?: LogFields;
|
|
1688
|
+
/** Nesting level; the root span is 0. */
|
|
1689
|
+
depth: number;
|
|
1690
|
+
durationMs: number;
|
|
1691
|
+
error?: {
|
|
1692
|
+
message: string;
|
|
1693
|
+
type: string;
|
|
1694
|
+
};
|
|
1695
|
+
name: string;
|
|
1696
|
+
/** Start of this span relative to the trace's start, in ms. */
|
|
1697
|
+
offsetMs: number;
|
|
1698
|
+
ok: boolean;
|
|
1699
|
+
parentSpanId: string;
|
|
1700
|
+
spanId: string;
|
|
1701
|
+
}
|
|
1702
|
+
/** One folded trace: the dispatch plus every span recorded beneath it. */
|
|
1703
|
+
interface TraceSummary {
|
|
1704
|
+
/** Wall-clock span of the whole trace (root start → last span end). */
|
|
1705
|
+
durationMs: number;
|
|
1706
|
+
functionPath: string;
|
|
1707
|
+
/** False when the root or any descendant span errored. */
|
|
1708
|
+
ok: boolean;
|
|
1709
|
+
/** Display name of the trace — the root span's name. */
|
|
1710
|
+
rootName: string;
|
|
1711
|
+
shardKey?: string;
|
|
1712
|
+
/**
|
|
1713
|
+
* Spans ordered by `(offsetMs, depth)`, ready to render as waterfall rows.
|
|
1714
|
+
* Start time alone is not enough to order them: spans are recorded on
|
|
1715
|
+
* completion and `startTs` has millisecond resolution, so a parent and its
|
|
1716
|
+
* child routinely tie. Breaking that tie by depth makes the sequence a valid
|
|
1717
|
+
* pre-order traversal of the span tree, so indenting each row by its `depth`
|
|
1718
|
+
* yields the nesting without a separate tree walk.
|
|
1719
|
+
*/
|
|
1720
|
+
spans: TraceSpan[];
|
|
1721
|
+
startTs: number;
|
|
1722
|
+
traceId: string;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* A bounded, in-memory ring of recent {@link SpanEvent}s (oldest evicted first),
|
|
1726
|
+
* mirroring `LogBuffer`. Spans arrive in *completion* order — a parent settles
|
|
1727
|
+
* after its children — so ordering is imposed by {@link foldTraces} at read
|
|
1728
|
+
* time rather than assumed here.
|
|
1729
|
+
*/
|
|
1730
|
+
declare class SpanBuffer {
|
|
1731
|
+
private readonly buffer;
|
|
1732
|
+
private readonly capacity;
|
|
1733
|
+
constructor(capacity?: number);
|
|
1734
|
+
/** Number of spans currently buffered. */
|
|
1735
|
+
get size(): number;
|
|
1736
|
+
/** Drop every buffered span. */
|
|
1737
|
+
clear(): void;
|
|
1738
|
+
/** Snapshot of the buffered spans in insertion order. Fresh array per call. */
|
|
1739
|
+
entries(): SpanEvent[];
|
|
1740
|
+
/**
|
|
1741
|
+
* Whether any buffered span belongs to `traceId`. A membership test rather
|
|
1742
|
+
* than `entries().some(...)` so the per-dispatch check that decides whether
|
|
1743
|
+
* to record a root span doesn't copy the whole ring on every request.
|
|
1744
|
+
*/
|
|
1745
|
+
hasTrace(traceId: string): boolean;
|
|
1746
|
+
/** Append a span, evicting the oldest when at capacity. */
|
|
1747
|
+
push(span: SpanEvent): void;
|
|
1748
|
+
}
|
|
1749
|
+
/** {@link foldTraces} result: the folded waterfalls plus the total distinct traces available before the `limit`. */
|
|
1750
|
+
interface FoldedTraces {
|
|
1751
|
+
/**
|
|
1752
|
+
* Distinct traces present in the buffer — the denominator for a "showing N of
|
|
1753
|
+
* M" affordance. `traces.length` is `min(total, limit)`, so `total > traces.length`
|
|
1754
|
+
* means older traces are held in the ring but not returned.
|
|
1755
|
+
*/
|
|
1756
|
+
total: number;
|
|
1757
|
+
/** The newest `limit` traces, folded into waterfalls. */
|
|
1758
|
+
traces: TraceSummary[];
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Group a flat span list into per-trace waterfalls, newest trace first, plus the
|
|
1762
|
+
* total number of distinct traces available (so a caller can report truncation).
|
|
1763
|
+
* @param spans Buffered spans, in arrival order.
|
|
1764
|
+
* @param limit Maximum number of traces to return, newest first.
|
|
1765
|
+
*/
|
|
1766
|
+
declare const foldTraces: (spans: ReadonlyArray<SpanEvent>, limit?: number) => FoldedTraces;
|
|
1767
|
+
/** One record field whose `v.storage()` value points at an object key absent from the bucket. */
|
|
1768
|
+
interface DanglingReference {
|
|
1769
|
+
/** The `v.storage()` column the dangling key was found in. */
|
|
1770
|
+
column: string;
|
|
1771
|
+
/** Primary key (`id`) of the owning row. */
|
|
1772
|
+
id: string;
|
|
1773
|
+
/** The object key the row references but which does not exist in the bucket. */
|
|
1774
|
+
key: string;
|
|
1775
|
+
/** The table the owning row lives in. */
|
|
1776
|
+
table: string;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Result of {@link findDanglingReferences}: the dangling references discovered
|
|
1780
|
+
* (record fields pointing at a missing object), plus `truncated` — `true` when a
|
|
1781
|
+
* scan/result cap clipped the set, so the studio can log/surface that the view is
|
|
1782
|
+
* partial. `scanned` is the total number of non-empty storage-field values
|
|
1783
|
+
* examined, so the studio can show "checked N references".
|
|
1784
|
+
*/
|
|
1785
|
+
interface DanglingReferenceResult {
|
|
1786
|
+
references: DanglingReference[];
|
|
1787
|
+
scanned: number;
|
|
1788
|
+
truncated: boolean;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Find every record storage-field value that points at an object key NOT present
|
|
1792
|
+
* in `liveKeys` — a dangling reference (the record references a file the bucket no
|
|
1793
|
+
* longer has). `storageColumns` is the schema-derived `{ table: [field, …] }` map
|
|
1794
|
+
* the codegen subclass supplies (empty for the base, schema-free DO); `liveKeys`
|
|
1795
|
+
* is the set of object keys that actually exist in the bucket (the caller passes
|
|
1796
|
+
* the enumerated bucket listing). Scans only the declared storage columns — never
|
|
1797
|
+
* the whole shard — and resolves each column to its physical/`__doc__` expression
|
|
1798
|
+
* with the same injection-safe, bound-parameter discipline as `readTablePage`.
|
|
1799
|
+
*
|
|
1800
|
+
* Bounded: at most {@link DANGLING_SCAN_CAP} rows per column are examined and at
|
|
1801
|
+
* most {@link DANGLING_RESULT_CAP} references returned; `truncated` flags either
|
|
1802
|
+
* cap firing. An empty `storageColumns` (an app that models no storage refs)
|
|
1803
|
+
* yields an empty, non-truncated result.
|
|
1804
|
+
*/
|
|
1805
|
+
declare const findDanglingReferences: (sql: SqlExec, storageColumns: Record<string, string[]>, liveKeys: Iterable<string>) => DanglingReferenceResult;
|
|
1806
|
+
/**
|
|
1807
|
+
* The trace ids a dispatch's spans hang off: taken from the inbound
|
|
1808
|
+
* `traceparent` when the runtime forwarded one (so the shard's spans join the
|
|
1809
|
+
* worker's trace and any container's beneath it), else freshly minted so a
|
|
1810
|
+
* dispatch with no inbound context — a subscription re-run, a server-initiated
|
|
1811
|
+
* call — still produces a coherent, self-contained local trace.
|
|
1812
|
+
*/
|
|
1813
|
+
declare const resolveTraceAnchor: (traceparent: string | undefined) => {
|
|
1814
|
+
rootSpanId: string;
|
|
1815
|
+
sampled: boolean;
|
|
1816
|
+
traceId: string;
|
|
1817
|
+
};
|
|
1818
|
+
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 DatabaseInstrumentation, type DatabaseTally, 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 FunctionMetricBucket, type FunctionMetricIndexHit, type HostTracingLike, 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 MetricHistoryOptions, type MetricHistoryPoint, type MetricHistorySeries, type MetricSeries, type MetricsDeps, type QueryStatEntry, REQUEST_LOG_TABLE, type RecordAuthEventInput, type RecordFunctionMetricInput, type RequestLogResult, type RequestLogWriteOptions, type SecurityAuditResult, type SecurityFinding, type SecurityFindingKind, type SecurityFindingLevel, SpanBuffer, type SpanCollection, type SpanCollector, type SpanHandle, type TraceAnchor, type TraceSpan, type TraceSummary, 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, resolveTraceAnchor, upsertIssueState };
|