@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.
- package/README.md +95 -56
- package/dist/bot/access-control.d.ts +89 -31
- package/dist/bot/access-control.d.ts.map +1 -1
- package/dist/bot/access-control.js +10 -28
- package/dist/bot/access-control.js.map +1 -1
- package/dist/bot/callbacks.d.ts +0 -13
- package/dist/bot/callbacks.d.ts.map +1 -1
- package/dist/bot/callbacks.js +0 -16
- package/dist/bot/callbacks.js.map +1 -1
- package/dist/bot/create.d.ts.map +1 -1
- package/dist/bot/create.js +8 -5
- package/dist/bot/create.js.map +1 -1
- package/dist/bot/kit.d.ts +4 -9
- package/dist/bot/kit.d.ts.map +1 -1
- package/dist/bot/kit.js +0 -1
- package/dist/bot/kit.js.map +1 -1
- package/dist/bot/lang.d.ts +46 -0
- package/dist/bot/lang.d.ts.map +1 -0
- package/dist/bot/lang.js +29 -0
- package/dist/bot/lang.js.map +1 -0
- package/dist/bot/language.d.ts +0 -2
- package/dist/bot/language.d.ts.map +1 -1
- package/dist/bot/language.js +0 -12
- package/dist/bot/language.js.map +1 -1
- package/dist/bot/llm.d.ts +1 -2
- package/dist/bot/llm.d.ts.map +1 -1
- package/dist/bot/llm.js +1 -2
- package/dist/bot/llm.js.map +1 -1
- package/dist/bot/menu.d.ts +7 -0
- package/dist/bot/menu.d.ts.map +1 -1
- package/dist/bot/menu.js +8 -2
- package/dist/bot/menu.js.map +1 -1
- package/dist/bot/payments/callbacks.d.ts +1 -1
- package/dist/bot/payments/callbacks.d.ts.map +1 -1
- package/dist/bot/payments/callbacks.js +9 -11
- package/dist/bot/payments/callbacks.js.map +1 -1
- package/dist/bot/payments/commands.d.ts +1 -1
- package/dist/bot/payments/commands.d.ts.map +1 -1
- package/dist/bot/payments/commands.js +2 -2
- package/dist/bot/payments/commands.js.map +1 -1
- package/dist/bot/payments/derive.d.ts.map +1 -1
- package/dist/bot/payments/derive.js +7 -6
- package/dist/bot/payments/derive.js.map +1 -1
- package/dist/bot/payments/handlers.d.ts +1 -5
- package/dist/bot/payments/handlers.d.ts.map +1 -1
- package/dist/bot/payments/handlers.js +59 -20
- package/dist/bot/payments/handlers.js.map +1 -1
- package/dist/bot/payments/invoice.d.ts.map +1 -1
- package/dist/bot/payments/invoice.js +27 -10
- package/dist/bot/payments/invoice.js.map +1 -1
- package/dist/bot/payments/menu-item.d.ts.map +1 -1
- package/dist/bot/payments/menu-item.js +2 -2
- package/dist/bot/payments/menu-item.js.map +1 -1
- package/dist/bot/payments/payouts.d.ts +3 -3
- package/dist/bot/payments/payouts.js +3 -3
- package/dist/bot/payments/plugin.d.ts +87 -29
- package/dist/bot/payments/plugin.d.ts.map +1 -1
- package/dist/bot/payments/refund.d.ts +1 -5
- package/dist/bot/payments/refund.d.ts.map +1 -1
- package/dist/bot/payments/refund.js +24 -42
- package/dist/bot/payments/refund.js.map +1 -1
- package/dist/bot/payments/types.d.ts +2 -9
- package/dist/bot/payments/types.d.ts.map +1 -1
- package/dist/bot/payments/types.js +0 -7
- package/dist/bot/payments/types.js.map +1 -1
- package/dist/bot/payments/waiver.d.ts +1 -17
- package/dist/bot/payments/waiver.d.ts.map +1 -1
- package/dist/bot/payments/waiver.js +5 -33
- package/dist/bot/payments/waiver.js.map +1 -1
- package/dist/bot/profile.d.ts.map +1 -1
- package/dist/bot/profile.js +18 -11
- package/dist/bot/profile.js.map +1 -1
- package/dist/bot/reactions.d.ts +0 -2
- package/dist/bot/reactions.d.ts.map +1 -1
- package/dist/bot/reactions.js +0 -16
- package/dist/bot/reactions.js.map +1 -1
- package/dist/bot/session.d.ts +1 -21
- package/dist/bot/session.d.ts.map +1 -1
- package/dist/bot/session.js +1 -29
- package/dist/bot/session.js.map +1 -1
- package/dist/bot/storage-d1.d.ts.map +1 -1
- package/dist/bot/storage-d1.js +4 -4
- package/dist/bot/storage-d1.js.map +1 -1
- package/dist/bot/storage.d.ts +1 -1
- package/dist/bot/storage.js +6 -6
- package/dist/bot/storage.js.map +1 -1
- package/dist/bot/worker.d.ts.map +1 -1
- package/dist/bot/worker.js +24 -15
- package/dist/bot/worker.js.map +1 -1
- package/dist/cli/text.d.ts.map +1 -1
- package/dist/cli/text.js +2 -3
- package/dist/cli/text.js.map +1 -1
- package/dist/llm/index.js +5 -5
- package/dist/llm/index.js.map +1 -1
- package/dist/offensive.d.ts.map +1 -1
- package/dist/offensive.js +3 -4
- package/dist/offensive.js.map +1 -1
- package/dist/runtime.d.ts +6 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +12 -17
- package/dist/runtime.js.map +1 -1
- package/dist/tg-html/index.d.ts +11 -4
- package/dist/tg-html/index.d.ts.map +1 -1
- package/dist/tg-html/index.js +13 -8
- package/dist/tg-html/index.js.map +1 -1
- package/dist/tg-md/index.d.ts +6 -6
- package/dist/tg-md/index.d.ts.map +1 -1
- package/dist/tg-md/index.js +9 -19
- package/dist/tg-md/index.js.map +1 -1
- package/dist/universal/format.d.ts +3 -2
- package/dist/universal/format.d.ts.map +1 -1
- package/dist/universal/format.js +3 -2
- package/dist/universal/format.js.map +1 -1
- package/dist/universal/log.d.ts +176 -95
- package/dist/universal/log.d.ts.map +1 -1
- package/dist/universal/log.js +677 -217
- package/dist/universal/log.js.map +1 -1
- package/dist/universal/time.d.ts +11 -2
- package/dist/universal/time.d.ts.map +1 -1
- package/dist/universal/time.js +22 -2
- package/dist/universal/time.js.map +1 -1
- package/package.json +9 -15
- package/dist/bot/index.d.ts +0 -31
- package/dist/bot/index.d.ts.map +0 -1
- package/dist/bot/index.js +0 -31
- package/dist/bot/index.js.map +0 -1
- package/dist/universal/currency/download-crypto-list.d.ts +0 -10
- package/dist/universal/currency/download-crypto-list.d.ts.map +0 -1
- package/dist/universal/currency/download-crypto-list.js +0 -69
- package/dist/universal/currency/download-crypto-list.js.map +0 -1
- package/dist/universal/url/update-rules.d.ts +0 -15
- package/dist/universal/url/update-rules.d.ts.map +0 -1
- package/dist/universal/url/update-rules.js +0 -122
- package/dist/universal/url/update-rules.js.map +0 -1
package/dist/universal/log.js
CHANGED
|
@@ -1,108 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* lines
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
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" : "
|
|
81
|
-
return sink === "browser" ? "human" : "
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
194
|
+
if (raw === "0")
|
|
98
195
|
return false;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
// ---
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
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(
|
|
243
|
-
const key =
|
|
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
|
-
|
|
619
|
+
line(ctx, "warn", message, fields);
|
|
250
620
|
},
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|