@adriangalilea/utils 8.0.0 → 9.1.0

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.
Files changed (79) hide show
  1. package/README.md +91 -33
  2. package/dist/bot/access-control.d.ts.map +1 -1
  3. package/dist/bot/access-control.js +9 -4
  4. package/dist/bot/access-control.js.map +1 -1
  5. package/dist/bot/coalesce.d.ts +2 -7
  6. package/dist/bot/coalesce.d.ts.map +1 -1
  7. package/dist/bot/coalesce.js +20 -9
  8. package/dist/bot/coalesce.js.map +1 -1
  9. package/dist/bot/create.d.ts.map +1 -1
  10. package/dist/bot/create.js +8 -5
  11. package/dist/bot/create.js.map +1 -1
  12. package/dist/bot/draft.d.ts.map +1 -1
  13. package/dist/bot/draft.js +12 -2
  14. package/dist/bot/draft.js.map +1 -1
  15. package/dist/bot/kit.d.ts +0 -2
  16. package/dist/bot/kit.d.ts.map +1 -1
  17. package/dist/bot/kit.js +7 -6
  18. package/dist/bot/kit.js.map +1 -1
  19. package/dist/bot/llm.d.ts.map +1 -1
  20. package/dist/bot/llm.js +10 -3
  21. package/dist/bot/llm.js.map +1 -1
  22. package/dist/bot/menu.d.ts.map +1 -1
  23. package/dist/bot/menu.js +6 -4
  24. package/dist/bot/menu.js.map +1 -1
  25. package/dist/bot/notify.d.ts +0 -25
  26. package/dist/bot/notify.d.ts.map +1 -1
  27. package/dist/bot/notify.js +4 -2
  28. package/dist/bot/notify.js.map +1 -1
  29. package/dist/bot/payments/handlers.d.ts.map +1 -1
  30. package/dist/bot/payments/handlers.js +58 -18
  31. package/dist/bot/payments/handlers.js.map +1 -1
  32. package/dist/bot/payments/invoice.d.ts.map +1 -1
  33. package/dist/bot/payments/invoice.js +26 -8
  34. package/dist/bot/payments/invoice.js.map +1 -1
  35. package/dist/bot/payments/menu-item.d.ts.map +1 -1
  36. package/dist/bot/payments/menu-item.js +8 -2
  37. package/dist/bot/payments/menu-item.js.map +1 -1
  38. package/dist/bot/payments/refund.d.ts.map +1 -1
  39. package/dist/bot/payments/refund.js +18 -10
  40. package/dist/bot/payments/refund.js.map +1 -1
  41. package/dist/bot/payments/waiver.js +3 -3
  42. package/dist/bot/payments/waiver.js.map +1 -1
  43. package/dist/bot/profile.d.ts.map +1 -1
  44. package/dist/bot/profile.js +18 -11
  45. package/dist/bot/profile.js.map +1 -1
  46. package/dist/bot/storage-d1.d.ts.map +1 -1
  47. package/dist/bot/storage-d1.js +4 -4
  48. package/dist/bot/storage-d1.js.map +1 -1
  49. package/dist/bot/storage.js +5 -5
  50. package/dist/bot/storage.js.map +1 -1
  51. package/dist/bot/worker.d.ts.map +1 -1
  52. package/dist/bot/worker.js +23 -11
  53. package/dist/bot/worker.js.map +1 -1
  54. package/dist/llm/index.d.ts.map +1 -1
  55. package/dist/llm/index.js +17 -5
  56. package/dist/llm/index.js.map +1 -1
  57. package/dist/metrics/index.d.ts.map +1 -1
  58. package/dist/metrics/index.js +3 -1
  59. package/dist/metrics/index.js.map +1 -1
  60. package/dist/offensive.d.ts.map +1 -1
  61. package/dist/offensive.js +3 -4
  62. package/dist/offensive.js.map +1 -1
  63. package/dist/platform/kev.d.ts +1 -2
  64. package/dist/platform/kev.d.ts.map +1 -1
  65. package/dist/platform/kev.js +21 -55
  66. package/dist/platform/kev.js.map +1 -1
  67. package/dist/runtime.d.ts +6 -0
  68. package/dist/runtime.d.ts.map +1 -1
  69. package/dist/runtime.js +12 -0
  70. package/dist/runtime.js.map +1 -1
  71. package/dist/universal/format.d.ts +3 -2
  72. package/dist/universal/format.d.ts.map +1 -1
  73. package/dist/universal/format.js +3 -2
  74. package/dist/universal/format.js.map +1 -1
  75. package/dist/universal/log.d.ts +180 -95
  76. package/dist/universal/log.d.ts.map +1 -1
  77. package/dist/universal/log.js +723 -217
  78. package/dist/universal/log.js.map +1 -1
  79. package/package.json +3 -2
@@ -1,108 +1,217 @@
1
1
  /**
2
- * Terminal-first logger with two renderings and one automatic decision.
2
+ * The logger: one stream of events, written the same way by go-utils,
3
+ * ts-utils and py-utils. The contract is the utils README
4
+ * (github.com/adriangalilea/utils); this file is the TypeScript side of it.
3
5
  *
4
- * Two knobs, never confusing: format = how lines render, time = whether
5
- * lines carry time. Each defaults sensibly and independently.
6
+ * Writing
7
+ * Every verb takes a message, then optional fields:
8
+ * log.warn("retrying", { attempt: 2 })
9
+ * log.error("charge failed", { err })
10
+ * The message says what happened, the fields say with what. Fields are a
11
+ * plain object; anything else panics.
6
12
  *
7
- * Format — LOG_FORMAT=human|record, or setLogFormat(). Default by TTY
8
- * detection on the sink: a TTY gets human (symbols, color, a line meant
9
- * for eyes), anything else gets record (level words, plain text, a line
10
- * meant for a file read later). Detection happens on the stream actually
11
- * written to (stderr), never a sibling stream.
13
+ * Verbs print at fixed levels
14
+ * error ⨯ · fail ⨯ (error) · warn ⚠ · warnOnce ⚠ (warn) · info · success ✓
15
+ * · wait ○ · ready ▶ · step • (info) · debug ◦ (debug) · trace » (trace).
16
+ * warnOnce fires once per message and scope (memory bounded).
12
17
  *
13
- * Time — LOG_TIME=1|0, or setLogTime(). Default: on in Node record mode,
14
- * off everywhere else (human eyes don't need stamps; browser and Worker
15
- * consoles stamp lines themselves). Record time opens the line with UTC
16
- * RFC3339; human time prefixes a dim local HH:MM:SS (a human knows
17
- * today's date).
18
+ * Scopes
19
+ * log.scope("api").scope("auth") carries ["api", "auth"]. Its threshold
20
+ * is read live from API_AUTH_LOG_LEVEL, then API_LOG_LEVEL, then
21
+ * LOG_LEVEL, then setLogLevel(), then info. Levels:
22
+ * silent < error < warn < info < debug < trace.
18
23
  *
19
- * The record line with time on, identical across go-utils and py-utils by
20
- * design:
21
- * 2026-08-07T12:34:56Z WARN [scope] message
22
- * UTC RFC3339 seconds precision · level word padded to 5 · scope brackets
23
- * only when scoped · no color, no symbols. Grep WARN, not ⚠.
24
+ * Spans
25
+ * A span is a piece of work written as two events (start, end) and a
26
+ * span is a logger: what is written through it carries its span_id and
27
+ * trace_id, nothing else does. There is no ambient context.
28
+ * await log.span("sync", { repo }, async (s) => {
29
+ * s.info("cloned", { files })
30
+ * await s.span("build", async (c) => { ... })
31
+ * s.set({ pushed: true })
32
+ * })
33
+ * span returns what fn returns; a throw ends the span failed (the error
34
+ * in fields.err) and rethrows. The open form, for work that does not fit
35
+ * a block: const s = log.begin("upload", { size }) ... s.end() or
36
+ * s.end(err). Ending twice, or set() after the end, panics.
37
+ * Whether a span is shown is decided once, at begin, by its start's level
38
+ * against its scope's threshold; the end follows, so a hidden span writes
39
+ * no end. A failure is never lost to that: a hidden span that fails
40
+ * writes one fail line (its name; its fields with err and dur_ms) as a
41
+ * line of the nearest shown span. What a hidden span holds attaches to the nearest
42
+ * shown span, a joined remote one included: its lines carry that id, its
43
+ * children and its traceparent() name it. With no shown ancestor its
44
+ * lines carry no ids. A viewer never meets a missing parent.
24
45
  *
25
- * Sinks
26
- * Node/Bun/Deno: every level writes to stderr through console.error, so
27
- * stdout stays clean for data and cli/live's console routing keeps log
28
- * lines above an active region.
29
- * Browser: level-mapped console methods (debug/info/warn/error), human.
30
- * Workers: level-mapped console methods, record.
46
+ * Traces cross processes explicitly
47
+ * s.traceparent() is the W3C value to hand on (HTTP header, or the
48
+ * TRACEPARENT env of a child). log.join(value) returns a logger whose
49
+ * spans are children of that remote span; its lines outside a span carry
50
+ * no ids. null/undefined returns the logger itself; a malformed value is
51
+ * ignored with one warn per distinct value. TRACEPARENT in the
52
+ * environment at process start is joined by every root span.
31
53
  *
32
- * Levels vs verbs
33
- * Levels filter: silent < error < warn < info < debug < trace. An unknown
34
- * LOG_LEVEL is a misconfiguration and throws Panic.
35
- * Verbs express outcome and are renderings, never levels:
36
- * error ⨯ fail ⨯ (error) · warn ⚠ (warn) · info · success ✓ · wait ○ ·
37
- * ready ▶ · step • (info) · debug ◦ (debug) · trace » (trace).
38
- * In record mode a verb renders as its level word.
54
+ * Renderings
55
+ * json: one event per line, keys in the contract's order, fields sorted
56
+ * at every depth. human: symbols, colour (TTY, NO_COLOR, FORCE_COLOR),
57
+ * fields dim as key=value, an error's stack under its line once (a span
58
+ * failing with the error a span inside it failed with shows the message;
59
+ * json writes it on both ends), spans as
60
+ * "○ name" ... "✓ name 254ms" with what they hold indented.
61
+ * Default: Node/Bun/Deno human when stderr is a TTY, else json; browser
62
+ * human; Worker json.
39
63
  *
40
- * Scopes
41
- * log.scope('api') returns a child whose lines carry [api]; scopes nest
42
- * and join: log.scope('api').scope('auth') → [api auth]. A scoped logger
43
- * resolves its threshold from {SCOPE}_LOG_LEVEL (upper-cased, joined and
44
- * sanitized to A-Z0-9_: API_AUTH_LOG_LEVEL, then API_LOG_LEVEL), falling
45
- * back to setLogLevel(), then LOG_LEVEL, then info — read live, so one
46
- * subsystem can be silenced or opened up without touching code.
64
+ * Configuration
65
+ * Each variable has a setter; the setter is the program's default and the
66
+ * variable, read live, overrides it: LOG_FORMAT=human|json (setLogFormat),
67
+ * LOG_TIME=1|0 (setLogTime, a dim local HH:MM:SS before human lines, off
68
+ * by default), LOG_LEVEL and {SCOPE}_LOG_LEVEL (setLogLevel), LOG_SERVICE
69
+ * (setLogService). Values match without regard to case, an empty variable
70
+ * is unset, an unknown value panics. The service defaults to the entry
71
+ * script's name without extension; in a Worker or browser it must be
72
+ * set, or the first json event panics.
47
73
  *
48
- * Config is env plus three runtime setters — setLogLevel(), setLogFormat(),
49
- * setLogTime(). No handlers, no formatters, no init. NO_COLOR /
50
- * FORCE_COLOR override color within human mode. Unknown values of any
51
- * knob throw Panic. All internal memory is bounded.
74
+ * Sinks
75
+ * Node/Bun/Deno write every event through console.error: stderr is the
76
+ * log, stdout stays data, and cli/live's console patch keeps lines above
77
+ * a live region. Browsers and Workers use the level's console method.
52
78
  *
53
- * Siblings: go-utils and py-utils implement the same doctrine in their own
54
- * idioms; the record line format is identical by design.
79
+ * The wire shape is exported as LogEvent, and parseEvent(line) reads one back.
55
80
  */
56
- import { panic } from "../offensive.js";
81
+ import { assert, panic, SourcedError } from "../offensive.js";
57
82
  import { runtime } from "../runtime.js";
58
83
  import { BOLD, DIM, fg, RESET } from "./ansi.js";
84
+ const EVENT_KEYS = [
85
+ "time",
86
+ "level",
87
+ "verb",
88
+ "msg",
89
+ "service",
90
+ "scope",
91
+ "phase",
92
+ "trace_id",
93
+ "span_id",
94
+ "parent_id",
95
+ "status",
96
+ "dur_ms",
97
+ "fields",
98
+ ];
99
+ // --- verbs and levels ------------------------------------------------------
100
+ const LEVELS = {
101
+ silent: 0,
102
+ error: 1,
103
+ warn: 2,
104
+ info: 3,
105
+ debug: 4,
106
+ trace: 5,
107
+ };
108
+ const VERBS = {
109
+ error: { level: "error", symbol: "⨯", style: BOLD + fg(1), indent: "" },
110
+ fail: { level: "error", symbol: "⨯", style: BOLD + fg(1), indent: "" },
111
+ warn: { level: "warn", symbol: "⚠", style: BOLD + fg(3), indent: "" },
112
+ info: { level: "info", symbol: " ", style: "", indent: "" },
113
+ success: { level: "info", symbol: "✓", style: BOLD + fg(2), indent: "" },
114
+ wait: { level: "info", symbol: "○", style: BOLD + fg(7), indent: "" },
115
+ ready: { level: "info", symbol: "▶", style: BOLD + fg(2), indent: "" },
116
+ step: { level: "info", symbol: "•", style: DIM, indent: " " },
117
+ debug: { level: "debug", symbol: "◦", style: fg(8), indent: "" },
118
+ trace: { level: "trace", symbol: "»", style: BOLD + fg(5), indent: "" },
119
+ };
120
+ const isLogLevel = (s) => Object.hasOwn(LEVELS, s);
121
+ const isVerb = (s) => Object.hasOwn(VERBS, s);
59
122
  const sink = runtime.isNode || runtime.isBun || runtime.isDeno
60
123
  ? "node"
61
124
  : runtime.isBrowser
62
125
  ? "browser"
63
126
  : "worker";
64
- let formatOverride;
65
- /** Force human or record rendering at runtime. Wins over LOG_FORMAT and auto-detection. */
127
+ // An empty variable reads as unset: `LOG_LEVEL= cmd` is how a shell unsets one.
128
+ const env = (key) => runtime.env(key) || undefined;
129
+ // Values match without regard to case, from the environment and from code alike.
130
+ function parseLevel(raw, source) {
131
+ const level = String(raw).toLowerCase();
132
+ if (!isLogLevel(level))
133
+ panic(`unknown ${source}:`, raw, "(want silent|error|warn|info|debug|trace)");
134
+ return level;
135
+ }
136
+ function parseFormat(raw, source) {
137
+ const format = String(raw).toLowerCase();
138
+ if (format !== "human" && format !== "json")
139
+ panic(`unknown ${source}:`, raw, "(want human|json)");
140
+ return format;
141
+ }
142
+ // Each setter is the program's default; the variable, read live, overrides it.
143
+ let levelSetting;
144
+ let formatSetting;
145
+ let timeSetting;
146
+ let serviceSetting;
147
+ /** The program's default level. LOG_LEVEL and {SCOPE}_LOG_LEVEL override it. */
148
+ export function setLogLevel(level) {
149
+ levelSetting = parseLevel(level, "log level");
150
+ }
151
+ /** The program's default rendering, human or json. LOG_FORMAT overrides it. */
66
152
  export function setLogFormat(format) {
67
- if (format !== "human" && format !== "record")
68
- panic("unknown log format:", format, "(want human|record)");
69
- formatOverride = format;
153
+ formatSetting = parseFormat(format, "log format");
70
154
  }
155
+ /** A dim local HH:MM:SS before human lines (json always carries time). LOG_TIME overrides it. */
156
+ export function setLogTime(on) {
157
+ if (typeof on !== "boolean")
158
+ panic("setLogTime wants a boolean, got:", on);
159
+ timeSetting = on;
160
+ }
161
+ /** The program's name in every event. LOG_SERVICE overrides it. */
162
+ export function setLogService(name) {
163
+ if (typeof name !== "string" || name.length === 0)
164
+ panic("setLogService wants a non-empty name, got:", name);
165
+ serviceSetting = name;
166
+ }
167
+ const scopeVar = (scope) => `${scope.map((s) => s.toUpperCase().replace(/[^A-Z0-9]+/g, "_")).join("_")}_LOG_LEVEL`;
168
+ // Read live on every event, so an operator can open one subsystem without a restart.
169
+ function threshold(scope) {
170
+ for (let i = scope.length; i > 0; i--) {
171
+ const name = scopeVar(scope.slice(0, i));
172
+ const raw = env(name);
173
+ if (raw !== undefined)
174
+ return LEVELS[parseLevel(raw, name)];
175
+ }
176
+ const raw = env("LOG_LEVEL");
177
+ if (raw !== undefined)
178
+ return LEVELS[parseLevel(raw, "LOG_LEVEL")];
179
+ return LEVELS[levelSetting ?? "info"];
180
+ }
181
+ const admits = (scope, level) => LEVELS[level] <= threshold(scope);
71
182
  function currentFormat() {
72
- if (formatOverride)
73
- return formatOverride;
74
- const env = runtime.env("LOG_FORMAT");
75
- if (env === "human" || env === "record")
76
- return env;
77
- if (env !== undefined)
78
- panic("unknown LOG_FORMAT:", env, "(want human|record)");
183
+ const raw = env("LOG_FORMAT");
184
+ if (raw !== undefined)
185
+ return parseFormat(raw, "LOG_FORMAT");
186
+ if (formatSetting)
187
+ return formatSetting;
79
188
  if (sink === "node")
80
- return runtime.stderr.isTTY ? "human" : "record";
81
- return sink === "browser" ? "human" : "record";
82
- }
83
- // --- time ----------------------------------------------------------------
84
- let timeOverride;
85
- /** Force time prefixes on or off at runtime. Wins over LOG_TIME and the defaults. */
86
- export function setLogTime(on) {
87
- timeOverride = on;
189
+ return runtime.stderr.isTTY ? "human" : "json";
190
+ return sink === "browser" ? "human" : "json";
88
191
  }
89
- // Default: on only for Node record mode. Human lines are for eyes; browser
90
- // and Worker consoles stamp every line themselves.
91
- function timeEnabled(format) {
92
- if (timeOverride !== undefined)
93
- return timeOverride;
94
- const env = runtime.env("LOG_TIME");
95
- if (env === "1")
192
+ function timeEnabled() {
193
+ const raw = env("LOG_TIME");
194
+ if (raw === undefined)
195
+ return timeSetting ?? false;
196
+ if (raw === "1")
96
197
  return true;
97
- if (env === "0")
198
+ if (raw === "0")
98
199
  return false;
99
- if (env !== undefined)
100
- panic("unknown LOG_TIME:", env, "(want 1|0)");
101
- return format === "record" && sink === "node";
200
+ return panic("unknown LOG_TIME:", raw, "(want 1|0)");
201
+ }
202
+ function service() {
203
+ const named = env("LOG_SERVICE") ?? serviceSetting;
204
+ if (named !== undefined)
205
+ return named;
206
+ const entry = runtime.entry();
207
+ if (entry === undefined)
208
+ panic("log service unknown: a Worker or browser must set LOG_SERVICE or call setLogService()");
209
+ const base = entry.split(/[\\/]/).pop() ?? "";
210
+ const name = base.replace(/\.[^.]*$/, "") || base;
211
+ assert(name.length > 0, "log service: no name in entry", entry);
212
+ return name;
102
213
  }
103
214
  function colorEnabled() {
104
- if (currentFormat() === "record")
105
- return false;
106
215
  if (runtime.env("NO_COLOR"))
107
216
  return false;
108
217
  if (runtime.env("FORCE_COLOR"))
@@ -113,77 +222,239 @@ function colorEnabled() {
113
222
  return false;
114
223
  return runtime.stderr.isTTY && runtime.env("TERM") !== "dumb";
115
224
  }
116
- // --- levels --------------------------------------------------------------
117
- const LEVELS = {
118
- silent: 0,
119
- error: 1,
120
- warn: 2,
121
- info: 3,
122
- debug: 4,
123
- trace: 5,
124
- };
125
- function parseLevel(raw) {
126
- const n = LEVELS[raw.toLowerCase()];
127
- if (n === undefined)
128
- panic("unknown log level:", raw, "(want silent|error|warn|info|debug|trace)");
129
- return n;
225
+ // --- ids and traceparent ---------------------------------------------------
226
+ // W3C forbids an all-zero id; at 64 bits a redraw is a formality.
227
+ function randomHex(bytes) {
228
+ for (;;) {
229
+ const b = crypto.getRandomValues(new Uint8Array(bytes));
230
+ if (b.some((x) => x !== 0))
231
+ return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
232
+ }
130
233
  }
131
- let levelOverride;
132
- /** Set the global log level at runtime. Wins over LOG_LEVEL; per-scope env vars still win over this. */
133
- export function setLogLevel(level) {
134
- levelOverride = parseLevel(level);
234
+ const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
235
+ function parseTraceparent(value) {
236
+ const m = TRACEPARENT_RE.exec(value.trim());
237
+ if (!m)
238
+ return undefined;
239
+ const [, version, trace, span] = m;
240
+ if (version === "ff" || /^0+$/.test(trace) || /^0+$/.test(span))
241
+ return undefined;
242
+ return { trace, span };
135
243
  }
136
- const envKey = (scopes) => `${scopes
137
- .map((s) => s.toUpperCase().replace(/[^A-Z0-9]+/g, "_"))
138
- .join("_")}_LOG_LEVEL`;
139
- // Most specific scope wins: [api auth] reads API_AUTH_LOG_LEVEL, then
140
- // API_LOG_LEVEL, then the runtime override, then LOG_LEVEL. Read live on
141
- // every line so a level flip needs no restart hook.
142
- function threshold(scopes) {
143
- for (let i = scopes.length; i > 0; i--) {
144
- const raw = runtime.env(envKey(scopes.slice(0, i)));
145
- if (raw !== undefined)
146
- return parseLevel(raw);
244
+ // Read once at load: the environment the process started with.
245
+ const PROCESS_TRACEPARENT = env("TRACEPARENT");
246
+ const PROCESS_PARENT = PROCESS_TRACEPARENT === undefined
247
+ ? undefined
248
+ : parseTraceparent(PROCESS_TRACEPARENT);
249
+ let processTraceparentWarned = false;
250
+ // Lexicographic by code point, as Go (UTF-8 bytes) and Python sort. JS string
251
+ // comparison is by UTF-16 unit, which disagrees once a surrogate meets U+E000..U+FFFF.
252
+ const lift = (c) => c >= 0xd800 && c <= 0xdfff ? c + 0x2000 : c >= 0xe000 ? c - 0x800 : c;
253
+ function byCodePoint(a, b) {
254
+ const n = Math.min(a.length, b.length);
255
+ for (let i = 0; i < n; i++) {
256
+ const x = a.charCodeAt(i);
257
+ const y = b.charCodeAt(i);
258
+ if (x !== y)
259
+ return lift(x) - lift(y);
147
260
  }
148
- if (levelOverride !== undefined)
149
- return levelOverride;
150
- const raw = runtime.env("LOG_LEVEL");
151
- return raw === undefined ? LEVELS.info : parseLevel(raw);
261
+ return a.length - b.length;
152
262
  }
153
- const VERBS = {
154
- error: { level: "error", symbol: "⨯", style: BOLD + fg(1), indent: "" },
155
- fail: { level: "error", symbol: "⨯", style: BOLD + fg(1), indent: "" },
156
- warn: { level: "warn", symbol: "⚠", style: BOLD + fg(3), indent: "" },
157
- info: { level: "info", symbol: " ", style: "", indent: "" },
158
- success: { level: "info", symbol: "✓", style: BOLD + fg(2), indent: "" },
159
- wait: { level: "info", symbol: "○", style: BOLD + fg(7), indent: "" },
160
- ready: { level: "info", symbol: "▶", style: BOLD + fg(2), indent: "" },
161
- step: { level: "info", symbol: "•", style: DIM, indent: " " },
162
- debug: { level: "debug", symbol: "◦", style: fg(8), indent: "" },
163
- trace: { level: "trace", symbol: "»", style: BOLD + fg(5), indent: "" },
164
- };
165
- const LEVEL_WORD = {
166
- error: "ERROR",
167
- warn: "WARN ",
168
- info: "INFO ",
169
- debug: "DEBUG",
170
- trace: "TRACE",
171
- };
172
- // --- rendering -----------------------------------------------------------
173
- const rfc3339 = () => `${new Date().toISOString().slice(0, 19)}Z`;
174
- const clockTime = () => new Date().toTimeString().slice(0, 8);
175
- function stringify(m) {
176
- if (typeof m === "string")
177
- return m;
178
- if (m instanceof Error)
179
- return m.stack ?? String(m);
263
+ function sortedObject(entries, ancestors) {
264
+ const kept = Array.from(entries).filter(([, v]) => v !== undefined);
265
+ kept.sort(([a], [b]) => byCodePoint(a, b));
266
+ return new Map(kept.map(([k, v]) => [k, toNode(v, ancestors)]));
267
+ }
268
+ function base64(bytes) {
269
+ let binary = "";
270
+ for (let i = 0; i < bytes.length; i += 0x8000)
271
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
272
+ return btoa(binary);
273
+ }
274
+ const errorType = (e) => e.constructor.name || e.name;
275
+ function typeName(v) {
276
+ if (v === null)
277
+ return "null";
278
+ if (typeof v !== "object")
279
+ return typeof v;
280
+ return (v.constructor?.name || "Object");
281
+ }
282
+ // type, message, then when present stack, cause, and a SourcedError's
283
+ // source, operation, status, context. An error's own keys keep this order.
284
+ function errorNode(e, ancestors) {
285
+ const node = new Map([
286
+ ["type", errorType(e)],
287
+ ["message", String(e.message)],
288
+ ]);
289
+ if (typeof e.stack === "string")
290
+ node.set("stack", e.stack);
291
+ if (e.cause !== undefined && e.cause !== null) {
292
+ const cause = e.cause;
293
+ node.set("cause", cause instanceof Error
294
+ ? toNode(cause, ancestors)
295
+ : new Map([
296
+ ["type", typeName(cause)],
297
+ [
298
+ "message",
299
+ typeof cause === "object"
300
+ ? encode(toNode(cause, ancestors))
301
+ : String(cause),
302
+ ],
303
+ ]));
304
+ }
305
+ if (e instanceof SourcedError) {
306
+ node.set("source", e.source);
307
+ node.set("operation", e.operation);
308
+ if (e.status !== undefined)
309
+ node.set("status", e.status);
310
+ if (Object.keys(e.context).length > 0)
311
+ node.set("context", toNode(e.context, ancestors));
312
+ }
313
+ return node;
314
+ }
315
+ /** A field value as JSON, by the contract's table. */
316
+ function toNode(v, ancestors) {
317
+ if (v === null || typeof v === "string" || typeof v === "boolean")
318
+ return v;
319
+ if (typeof v === "number")
320
+ return Number.isFinite(v) ? v : String(v);
321
+ if (typeof v === "bigint")
322
+ return v.toString();
323
+ if (typeof v !== "object")
324
+ return String(v);
325
+ if (ancestors.has(v))
326
+ return "[circular]";
327
+ if (v instanceof Date)
328
+ return Number.isNaN(v.getTime()) ? String(v) : v.toISOString();
329
+ if (v instanceof Uint8Array)
330
+ return base64(v);
331
+ if (v instanceof ArrayBuffer)
332
+ return base64(new Uint8Array(v));
333
+ ancestors.add(v);
180
334
  try {
181
- return JSON.stringify(m) ?? String(m);
335
+ if (v instanceof Error)
336
+ return errorNode(v, ancestors);
337
+ if (Array.isArray(v))
338
+ return v.map((x) => (x === undefined ? null : toNode(x, ancestors)));
339
+ if (v instanceof Map)
340
+ return sortedObject(Array.from(v, ([k, x]) => [String(k), x]), ancestors);
341
+ if (v instanceof Set)
342
+ return Array.from(v, (x) => x === undefined ? null : toNode(x, ancestors));
343
+ const toJSON = v.toJSON;
344
+ if (typeof toJSON === "function")
345
+ return toNode(toJSON.call(v), ancestors);
346
+ return sortedObject(Object.entries(v), ancestors);
182
347
  }
183
- catch {
184
- return String(m);
348
+ finally {
349
+ ancestors.delete(v);
185
350
  }
186
351
  }
352
+ function encode(n) {
353
+ if (n === null)
354
+ return "null";
355
+ if (typeof n === "string" || typeof n === "number")
356
+ return JSON.stringify(n);
357
+ if (typeof n === "boolean")
358
+ return n ? "true" : "false";
359
+ if (Array.isArray(n))
360
+ return `[${n.map(encode).join(",")}]`;
361
+ const parts = [];
362
+ for (const [k, v] of n)
363
+ parts.push(`${JSON.stringify(k)}:${encode(v)}`);
364
+ return `{${parts.join(",")}}`;
365
+ }
366
+ const isPlainObject = (v) => {
367
+ if (typeof v !== "object" || v === null)
368
+ return false;
369
+ const proto = Object.getPrototypeOf(v);
370
+ return proto === Object.prototype || proto === null;
371
+ };
372
+ function assertFields(fields) {
373
+ assert(fields === undefined || isPlainObject(fields), "log fields must be a plain object (message, { key: value }), got", typeName(fields));
374
+ }
375
+ const round3 = (ms) => Math.round(ms * 1000) / 1000;
376
+ function json(d, now) {
377
+ const level = VERBS[d.verb].level;
378
+ let s = `{"time":"${now.toISOString()}","level":"${level}","verb":"${d.verb}","msg":${JSON.stringify(d.msg)},"service":${JSON.stringify(service())}`;
379
+ if (d.scope.length > 0)
380
+ s += `,"scope":${encode([...d.scope])}`;
381
+ if (d.phase)
382
+ s += `,"phase":"${d.phase}"`;
383
+ if (d.trace)
384
+ s += `,"trace_id":"${d.trace}"`;
385
+ if (d.span)
386
+ s += `,"span_id":"${d.span}"`;
387
+ if (d.parent)
388
+ s += `,"parent_id":"${d.parent}"`;
389
+ if (d.status)
390
+ s += `,"status":"${d.status}"`;
391
+ if (d.dur !== undefined)
392
+ s += `,"dur_ms":${round3(d.dur)}`;
393
+ if (d.fields) {
394
+ const node = sortedObject(Object.entries(d.fields), new Set());
395
+ if (node.size > 0)
396
+ s += `,"fields":${encode(node)}`;
397
+ }
398
+ return `${s}}`;
399
+ }
400
+ function humanDuration(ms) {
401
+ if (ms < 1000)
402
+ return `${Math.round(ms)}ms`;
403
+ if (ms < 60_000)
404
+ return `${(ms / 1000).toFixed(1)}s`;
405
+ const s = Math.round(ms / 1000);
406
+ return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
407
+ }
408
+ const quoted = (s) => s === "" || /[\s"=]/.test(s) ? JSON.stringify(s) : s;
409
+ const errorLine = (e) => e instanceof SourcedError
410
+ ? `${errorType(e)} [${e.source}:${e.operation}${e.status === undefined ? "" : ` ${e.status}`}]: ${e.message}`
411
+ : `${errorType(e)}: ${e.message}`;
412
+ function humanValue(v) {
413
+ if (v instanceof Error)
414
+ return quoted(errorLine(v));
415
+ const node = toNode(v, new Set());
416
+ return typeof node === "string" ? quoted(node) : encode(node);
417
+ }
418
+ // The stack minus its "Name: message" head (already on the line), then each cause.
419
+ function stackLines(e) {
420
+ const lines = [];
421
+ const seen = new Set();
422
+ let at = e;
423
+ while (at instanceof Error && !seen.has(at)) {
424
+ seen.add(at);
425
+ if (at !== e)
426
+ lines.push(`caused by ${errorLine(at)}`);
427
+ const stack = at.stack ?? "";
428
+ const head = String(at);
429
+ const body = stack.startsWith(head) ? stack.slice(head.length) : stack;
430
+ for (const l of body.split("\n"))
431
+ if (l.trim())
432
+ lines.push(l.trim());
433
+ at = at.cause;
434
+ }
435
+ return lines;
436
+ }
437
+ function human(d, now, clock) {
438
+ const v = VERBS[d.verb];
439
+ const color = colorEnabled();
440
+ const paint = (style, s) => color && style ? `${style}${s}${RESET}` : s;
441
+ const pad = " ".repeat(d.depth);
442
+ const stamp = clock ? `${paint(DIM, now.toTimeString().slice(0, 8))} ` : "";
443
+ const scope = d.scope.length > 0 ? `${paint(DIM, `[${d.scope.join(" ")}]`)} ` : "";
444
+ const dur = d.dur === undefined ? "" : ` ${paint(DIM, humanDuration(d.dur))}`;
445
+ const pairs = Object.entries(d.shows ?? d.fields ?? {})
446
+ .filter(([, x]) => x !== undefined)
447
+ .sort(([a], [b]) => byCodePoint(a, b));
448
+ const tail = pairs.length > 0
449
+ ? ` ${paint(DIM, pairs.map(([k, x]) => `${k}=${humanValue(x)}`).join(" "))}`
450
+ : "";
451
+ const head = `${stamp}${pad}${v.indent} ${paint(v.style, v.symbol)} ${scope}${d.msg}${dur}${tail}`;
452
+ // A stack prints once: an error passed up from an inner span shows its message only.
453
+ const stacks = pairs.flatMap(([, x]) => x instanceof Error && x !== d.printed
454
+ ? stackLines(x).map((l) => paint(DIM, `${pad} ${l}`))
455
+ : []);
456
+ return [head, ...stacks].join("\n");
457
+ }
187
458
  const CONSOLE_METHOD = {
188
459
  error: "error",
189
460
  warn: "warn",
@@ -191,44 +462,187 @@ const CONSOLE_METHOD = {
191
462
  debug: "debug",
192
463
  trace: "debug",
193
464
  };
194
- function emit(scopes, verb, messages) {
195
- const v = VERBS[verb];
196
- if (LEVELS[v.level] > threshold(scopes))
197
- return;
198
- // Node funnels every level through console.error: stderr is the log sink
199
- // and cli/live patches console methods to reroute lines above an active
200
- // region. Browser/worker use level-mapped methods so platform consoles
201
- // classify lines correctly.
202
- const method = sink === "node" ? "error" : CONSOLE_METHOD[v.level];
465
+ // The caller has already decided the event passes its threshold.
466
+ function emit(d) {
467
+ const now = new Date();
468
+ // Every knob is read on every event, so a bad value screams whichever rendering is on.
203
469
  const format = currentFormat();
204
- if (format === "record") {
205
- const stamp = timeEnabled(format) ? `${rfc3339()} ` : "";
206
- const scope = scopes.length > 0 ? `[${scopes.join(" ")}] ` : "";
207
- console[method](`${stamp}${LEVEL_WORD[v.level]} ${scope}${messages.map(stringify).join(" ")}`);
208
- return;
209
- }
210
- const color = colorEnabled();
211
- const dimmed = (s) => (color ? `${DIM}${s}${RESET}` : s);
212
- const stamp = timeEnabled(format) ? `${dimmed(clockTime())} ` : "";
213
- const symbol = color ? `${v.style}${v.symbol}${RESET}` : v.symbol;
214
- const scope = scopes.length > 0 ? `${dimmed(`[${scopes.join(" ")}]`)} ` : "";
215
- const prefix = `${stamp}${v.indent} ${symbol} ${scope}`;
216
- // A single string collapses into one line; anything richer is handed to
217
- // console so devtools/util.inspect keep live object rendering.
218
- if (messages.length === 1 && typeof messages[0] === "string") {
219
- console[method](`${prefix}${messages[0]}`);
220
- }
221
- else {
222
- console[method](prefix.trimEnd(), ...messages);
223
- }
470
+ const clock = timeEnabled();
471
+ const text = format === "json" ? json(d, now) : human(d, now, clock);
472
+ // Node: everything to stderr through console.error, which cli/live patches
473
+ // to keep lines above an active region. Elsewhere the level's method, so
474
+ // platform consoles classify the line.
475
+ if (sink === "node")
476
+ console.error(text);
477
+ else
478
+ console[CONSOLE_METHOD[VERBS[d.verb].level]](text);
224
479
  }
225
- // --- bounded state (daemons run for months) ------------------------------
226
480
  const BOUND = 1024;
227
481
  const warned = new Set();
228
- const timers = new Map();
229
- function makeLogger(scopes) {
230
- const verb = (v) => (...messages) => emit(scopes, v, messages);
231
- return {
482
+ const badTraceparents = new Set();
483
+ function line(ctx, verb, message, fields) {
484
+ assert(typeof message === "string", "log message must be a string, got", typeName(message));
485
+ assertFields(fields);
486
+ if (!admits(ctx.scope, VERBS[verb].level))
487
+ return;
488
+ emit({
489
+ verb,
490
+ msg: message,
491
+ scope: ctx.scope,
492
+ depth: ctx.depth,
493
+ trace: ctx.owner === undefined ? undefined : ctx.trace,
494
+ span: ctx.owner,
495
+ fields,
496
+ });
497
+ }
498
+ function open(ctx, name, initial) {
499
+ assert(typeof name === "string" && name.length > 0, "span name must be a non-empty string, got", typeName(name));
500
+ assertFields(initial);
501
+ let remote;
502
+ if (ctx.trace === undefined) {
503
+ remote = PROCESS_PARENT;
504
+ if (PROCESS_TRACEPARENT !== undefined &&
505
+ !remote &&
506
+ !processTraceparentWarned) {
507
+ processTraceparentWarned = true;
508
+ line(ctx, "warn", "malformed TRACEPARENT ignored", {
509
+ traceparent: PROCESS_TRACEPARENT,
510
+ });
511
+ }
512
+ }
513
+ const trace = ctx.trace ?? remote?.trace ?? randomHex(16);
514
+ const parent = ctx.trace === undefined ? remote?.span : ctx.parent;
515
+ const id = randomHex(8);
516
+ // Decided once, here: the start's level against the scope's threshold now.
517
+ const shown = admits(ctx.scope, "info");
518
+ const began = performance.now();
519
+ const fields = { ...initial };
520
+ let added = {};
521
+ let ended = false;
522
+ if (shown)
523
+ emit({
524
+ verb: "wait",
525
+ msg: name,
526
+ scope: ctx.scope,
527
+ depth: ctx.depth,
528
+ phase: "start",
529
+ trace,
530
+ span: id,
531
+ parent,
532
+ fields,
533
+ });
534
+ // A hidden span is transparent: its lines and children attach to the nearest
535
+ // shown span, a joined remote one included (it is shown in its own process's
536
+ // stream). With none, its lines carry no ids, as lines outside any span.
537
+ const caught = new Set();
538
+ const inner = shown
539
+ ? {
540
+ scope: ctx.scope,
541
+ depth: ctx.depth + 1,
542
+ trace,
543
+ parent: id,
544
+ owner: id,
545
+ caught,
546
+ }
547
+ : {
548
+ scope: ctx.scope,
549
+ depth: ctx.depth,
550
+ trace,
551
+ parent,
552
+ owner: parent,
553
+ caught,
554
+ };
555
+ return Object.assign(makeLogger(inner), {
556
+ set(more) {
557
+ assert(!ended, "span", name, "got set() after it ended");
558
+ assertFields(more);
559
+ added = { ...added, ...more };
560
+ },
561
+ // A hidden span hands on the nearest shown span (local or joined), its own id only with none.
562
+ traceparent() {
563
+ return `00-${trace}-${shown ? id : (parent ?? id)}-01`;
564
+ },
565
+ end(err) {
566
+ assert(!ended, "span", name, "ended twice");
567
+ ended = true;
568
+ const dur = performance.now() - began;
569
+ const failed = err !== undefined && err !== null;
570
+ const printed = err instanceof Error && caught.has(err) ? err : undefined;
571
+ // What spans inside this one printed stays printed for the spans around it.
572
+ for (const e of caught)
573
+ ctx.caught?.add(e);
574
+ if (!shown) {
575
+ // The end follows the decision made at begin: no end without its
576
+ // start. A failure is never lost to it: one line of the nearest
577
+ // shown span says the span failed.
578
+ if (!failed || !admits(ctx.scope, "error"))
579
+ return;
580
+ if (err instanceof Error)
581
+ ctx.caught?.add(err);
582
+ emit({
583
+ verb: "fail",
584
+ msg: name,
585
+ scope: ctx.scope,
586
+ depth: inner.depth,
587
+ trace: inner.owner === undefined ? undefined : trace,
588
+ span: inner.owner,
589
+ fields: { ...fields, ...added, err, dur_ms: round3(dur) },
590
+ printed,
591
+ });
592
+ return;
593
+ }
594
+ if (failed && err instanceof Error)
595
+ ctx.caught?.add(err);
596
+ const shows = failed ? { ...added, err } : added;
597
+ emit({
598
+ verb: failed ? "fail" : "success",
599
+ msg: name,
600
+ scope: ctx.scope,
601
+ depth: ctx.depth,
602
+ phase: "end",
603
+ trace,
604
+ span: id,
605
+ parent,
606
+ status: failed ? "error" : "ok",
607
+ dur,
608
+ fields: { ...fields, ...shows },
609
+ shows,
610
+ printed,
611
+ });
612
+ },
613
+ });
614
+ }
615
+ const isThenable = (v) => typeof v?.then === "function";
616
+ function makeLogger(ctx) {
617
+ const verb = (v) => (message, fields) => line(ctx, v, message, fields);
618
+ function span(name, a, b) {
619
+ const fn = typeof a === "function" ? a : b;
620
+ const fields = typeof a === "function" ? undefined : a;
621
+ assert(typeof fn === "function", "span", name, "needs a function");
622
+ const s = open(ctx, name, fields);
623
+ // end(null) means ok; a block that threw null still failed.
624
+ const failed = (e) => e ?? String(e);
625
+ let result;
626
+ try {
627
+ result = fn(s);
628
+ }
629
+ catch (e) {
630
+ s.end(failed(e));
631
+ throw e;
632
+ }
633
+ if (!isThenable(result)) {
634
+ s.end();
635
+ return result;
636
+ }
637
+ return Promise.resolve(result).then((v) => {
638
+ s.end();
639
+ return v;
640
+ }, (e) => {
641
+ s.end(failed(e));
642
+ throw e;
643
+ });
644
+ }
645
+ const logger = {
232
646
  error: verb("error"),
233
647
  fail: verb("fail"),
234
648
  warn: verb("warn"),
@@ -239,57 +653,149 @@ function makeLogger(scopes) {
239
653
  step: verb("step"),
240
654
  debug: verb("debug"),
241
655
  trace: verb("trace"),
242
- warnOnce(...messages) {
243
- const key = scopes.join(" ") + messages.map(stringify).join(" ");
656
+ warnOnce(message, fields) {
657
+ const key = `${ctx.scope.join("\0")}\0${message}`;
244
658
  if (warned.has(key))
245
659
  return;
246
660
  if (warned.size >= BOUND)
247
661
  warned.clear();
248
662
  warned.add(key);
249
- emit(scopes, "warn", messages);
663
+ line(ctx, "warn", message, fields);
250
664
  },
251
- time(label) {
252
- if (timers.size >= BOUND)
253
- timers.clear();
254
- timers.set(`${scopes.join(" ")}:${label}`, Date.now());
665
+ scope(name) {
666
+ assert(typeof name === "string" && name.length > 0, "scope name must be a non-empty string, got", typeName(name));
667
+ return makeLogger({ ...ctx, scope: [...ctx.scope, name] });
255
668
  },
256
- timeEnd(label) {
257
- const key = `${scopes.join(" ")}:${label}`;
258
- const start = timers.get(key);
259
- if (start === undefined) {
260
- emit(scopes, "warn", [`Timer '${label}' does not exist`]);
261
- return;
262
- }
263
- timers.delete(key);
264
- const ms = Date.now() - start;
265
- const pretty = ms > 10_000 ? `${Math.round(ms / 100) / 10}s` : `${Math.round(ms)}ms`;
266
- emit(scopes, "trace", [`${label}: ${pretty}`]);
669
+ span,
670
+ begin(name, fields) {
671
+ return open(ctx, name, fields);
267
672
  },
268
- scope(name) {
269
- return makeLogger([...scopes, name]);
673
+ join(traceparent) {
674
+ if (traceparent === null || traceparent === undefined)
675
+ return logger;
676
+ const remote = typeof traceparent === "string"
677
+ ? parseTraceparent(traceparent)
678
+ : undefined;
679
+ if (!remote) {
680
+ // Input from outside: one warn per distinct value, memory bounded.
681
+ const value = String(traceparent);
682
+ if (!badTraceparents.has(value)) {
683
+ if (badTraceparents.size >= BOUND)
684
+ badTraceparents.clear();
685
+ badTraceparents.add(value);
686
+ line(ctx, "warn", "malformed traceparent ignored", {
687
+ traceparent: value,
688
+ });
689
+ }
690
+ return logger;
691
+ }
692
+ return makeLogger({
693
+ scope: ctx.scope,
694
+ depth: ctx.depth,
695
+ trace: remote.trace,
696
+ parent: remote.span,
697
+ owner: undefined,
698
+ caught: ctx.caught,
699
+ });
270
700
  },
271
701
  };
702
+ return logger;
703
+ }
704
+ /** The root logger. */
705
+ export const log = makeLogger({
706
+ scope: [],
707
+ depth: 0,
708
+ trace: undefined,
709
+ parent: undefined,
710
+ owner: undefined,
711
+ caught: undefined,
712
+ });
713
+ // --- reading the stream ----------------------------------------------------
714
+ const TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
715
+ const HEX32 = /^[0-9a-f]{32}$/;
716
+ const HEX16 = /^[0-9a-f]{16}$/;
717
+ /**
718
+ * One line of a stream as an event, or null for a line that is not one (a
719
+ * runtime panic, a library writing to stderr itself) so a viewer can pass it
720
+ * through. A JSON object with a "verb" key claims to be an event: if it
721
+ * breaks the contract's shape, that is a bug in a writer and it panics.
722
+ */
723
+ export function parseEvent(text) {
724
+ const trimmed = text.trim();
725
+ if (!trimmed.startsWith("{"))
726
+ return null;
727
+ let value;
728
+ try {
729
+ value = JSON.parse(trimmed);
730
+ }
731
+ catch {
732
+ return null;
733
+ }
734
+ if (!isPlainObject(value) || !Object.hasOwn(value, "verb"))
735
+ return null;
736
+ const e = value;
737
+ const bad = (why) => panic("malformed log event:", why, "in", trimmed);
738
+ const has = (k) => Object.hasOwn(e, k);
739
+ const keys = Object.keys(e);
740
+ for (const k of keys)
741
+ if (!EVENT_KEYS.includes(k))
742
+ bad(`unknown key "${k}"`);
743
+ const order = EVENT_KEYS.filter(has);
744
+ if (keys.join() !== order.join())
745
+ bad(`keys out of order: ${keys.join(",")}`);
746
+ for (const k of ["time", "level", "verb", "msg", "service"])
747
+ if (!has(k))
748
+ bad(`missing "${k}"`);
749
+ if (typeof e.time !== "string" || !TIME_RE.test(e.time))
750
+ bad("time");
751
+ if (typeof e.verb !== "string" || !isVerb(e.verb))
752
+ bad("verb");
753
+ const verb = e.verb;
754
+ if (e.level !== VERBS[verb].level)
755
+ bad(`level of verb ${verb}`);
756
+ if (typeof e.msg !== "string")
757
+ bad("msg");
758
+ if (typeof e.service !== "string" || e.service.length === 0)
759
+ bad("service");
760
+ if (has("scope") &&
761
+ !(Array.isArray(e.scope) &&
762
+ e.scope.length > 0 &&
763
+ e.scope.every((s) => typeof s === "string")))
764
+ bad("scope");
765
+ if (has("trace_id") &&
766
+ !(typeof e.trace_id === "string" && HEX32.test(e.trace_id)))
767
+ bad("trace_id");
768
+ for (const k of ["span_id", "parent_id"])
769
+ if (has(k) && !(typeof e[k] === "string" && HEX16.test(e[k])))
770
+ bad(k);
771
+ if (has("fields") && !isPlainObject(e.fields))
772
+ bad("fields");
773
+ if (has("span_id") && !has("trace_id"))
774
+ bad("span_id without trace_id");
775
+ if (has("parent_id") && !has("phase"))
776
+ bad("parent_id without phase");
777
+ if (has("phase")) {
778
+ if (e.phase !== "start" && e.phase !== "end")
779
+ bad("phase");
780
+ if (!has("trace_id") || !has("span_id"))
781
+ bad("span event without ids");
782
+ }
783
+ if (e.phase === "start") {
784
+ if (verb !== "wait")
785
+ bad("start verb");
786
+ if (has("status") || has("dur_ms"))
787
+ bad("start with status or dur_ms");
788
+ }
789
+ if (e.phase === "end") {
790
+ if (e.status !== "ok" && e.status !== "error")
791
+ bad("status");
792
+ if (typeof e.dur_ms !== "number" || !(e.dur_ms >= 0))
793
+ bad("dur_ms");
794
+ if (verb !== (e.status === "ok" ? "success" : "fail"))
795
+ bad("end verb");
796
+ }
797
+ else if (has("status") || has("dur_ms"))
798
+ bad("status or dur_ms outside an end");
799
+ return e;
272
800
  }
273
- const root = makeLogger([]);
274
- export const error = root.error;
275
- export const fail = root.fail;
276
- export const warn = root.warn;
277
- export const warnOnce = root.warnOnce;
278
- export const info = root.info;
279
- export const success = root.success;
280
- export const wait = root.wait;
281
- export const ready = root.ready;
282
- export const step = root.step;
283
- export const debug = root.debug;
284
- export const trace = root.trace;
285
- export const time = root.time;
286
- export const timeEnd = root.timeEnd;
287
- export const scope = root.scope;
288
- /** The root logger plus its runtime knobs, for namespace-style consumers. */
289
- export const log = {
290
- ...root,
291
- setLogLevel,
292
- setLogFormat,
293
- setLogTime,
294
- };
295
801
  //# sourceMappingURL=log.js.map