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