@blamejs/core 0.7.18 → 0.7.19
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/CHANGELOG.md +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/log.js
CHANGED
|
@@ -1,500 +1,500 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* log — structured JSON application logger with request-id correlation.
|
|
4
|
-
*
|
|
5
|
-
* Distinct concern from lib/logger.js: logger.js is the framework's
|
|
6
|
-
* own boot/operational chatter to console with `[blamejs:<name>] `
|
|
7
|
-
* prefix (humans watching `npm start`). lib/log.js is the app-level
|
|
8
|
-
* structured logger meant to be ingested by a log aggregator.
|
|
9
|
-
*
|
|
10
|
-
* Each line is one JSON object on a single line, terminated with `\n`.
|
|
11
|
-
* Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
|
|
12
|
-
* Default routing: debug / info / warn → stdout; error / fatal → stderr.
|
|
13
|
-
* Multi-sink config (`sinks: [...]`) takes full control of routing.
|
|
14
|
-
*
|
|
15
|
-
* var log = b.log.create({
|
|
16
|
-
* level: "info", // env LOG_LEVEL > opts.level > "info"
|
|
17
|
-
* base: { service: "myapp", version: "1.2.3" },
|
|
18
|
-
* redact: true, // run extras through lib/redact
|
|
19
|
-
* });
|
|
20
|
-
*
|
|
21
|
-
* // Multi-sink: each sink gets every line at-or-above its own level.
|
|
22
|
-
* // Default (no `sinks` opt) splits info-and-below to stdout and
|
|
23
|
-
* // warn-and-up to stderr — same as before.
|
|
24
|
-
* var log = b.log.create({
|
|
25
|
-
* level: "debug",
|
|
26
|
-
* sinks: [
|
|
27
|
-
* { stream: process.stdout, level: "info" },
|
|
28
|
-
* { stream: fs.createWriteStream("./logs/debug.log"), level: "debug" },
|
|
29
|
-
* { stream: fs.createWriteStream("./logs/errors.log"), level: "error" },
|
|
30
|
-
* ],
|
|
31
|
-
* });
|
|
32
|
-
* // sinks: [...] is mutually exclusive with destination/errorDestination.
|
|
33
|
-
*
|
|
34
|
-
* log.info("user logged in", { userId: "u-1" });
|
|
35
|
-
* log.error("payment failed", { orderId, err: e.message });
|
|
36
|
-
*
|
|
37
|
-
* // Child with bound context
|
|
38
|
-
* var authLog = log.bind({ component: "auth" });
|
|
39
|
-
* authLog.info("password verified", { userId: "u-1" });
|
|
40
|
-
*
|
|
41
|
-
* // Request correlation via AsyncLocalStorage (Node async context)
|
|
42
|
-
* await log.runWithRequestId("req-abc", async function () {
|
|
43
|
-
* log.info("inside request"); // → ..., "requestId": "req-abc"
|
|
44
|
-
* });
|
|
45
|
-
*
|
|
46
|
-
* // Router middleware that allocates a requestId and binds it for
|
|
47
|
-
* // the entire request async chain
|
|
48
|
-
* r.use(log.middleware());
|
|
49
|
-
*
|
|
50
|
-
* Field merge order (last wins):
|
|
51
|
-
* 1. base context from create()
|
|
52
|
-
* 2. bound context from bind() (each ancestor up the chain)
|
|
53
|
-
* 3. requestId from ALS (if set)
|
|
54
|
-
* 4. extra arg from .info(msg, extra)
|
|
55
|
-
* 5. core fields: timestamp, level, message
|
|
56
|
-
*
|
|
57
|
-
* Core fields cannot be overwritten by extras — log.info("hi", { level: "X" })
|
|
58
|
-
* keeps level: "info" in the emitted line, with an _overwriteAttempt
|
|
59
|
-
* flag if the operator tried to clobber.
|
|
60
|
-
*/
|
|
61
|
-
|
|
62
|
-
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
63
|
-
var C = require("./constants");
|
|
64
|
-
var lazyRequire = require("./lazy-require");
|
|
65
|
-
var redact = require("./redact");
|
|
66
|
-
var safeBuffer = require("./safe-buffer");
|
|
67
|
-
var validateOpts = require("./validate-opts");
|
|
68
|
-
var { FrameworkError } = require("./framework-error");
|
|
69
|
-
|
|
70
|
-
// Lazy resolves to break load-order cycles:
|
|
71
|
-
// - parsers/safe-env requires log (boot logger for env-read errors).
|
|
72
|
-
// log can't require safe-env at top because safe-env's top-level
|
|
73
|
-
// `var log = boot("env")` would see log's exports half-built.
|
|
74
|
-
// - crypto requires constants + framework-error + vendor; it does NOT
|
|
75
|
-
// require log, so a top-of-file require would technically work, but
|
|
76
|
-
// keeping crypto lazy keeps log loadable from anywhere without
|
|
77
|
-
// pulling the whole crypto bundle into the framework's earliest
|
|
78
|
-
// boot path (request-id middleware needs only generateToken).
|
|
79
|
-
var safeEnv = lazyRequire(function () { return require("./parsers/safe-env"); });
|
|
80
|
-
var crypto = lazyRequire(function () { return require("./crypto"); });
|
|
81
|
-
|
|
82
|
-
// Request-id correlation token — 8 bytes hex-encoded (16 chars). Short
|
|
83
|
-
// enough to read in a log line, long enough to keep collisions far below
|
|
84
|
-
// audible-noise even at 1M req/s sustained. Routed through C.BYTES so
|
|
85
|
-
// the file's byte arithmetic has a single source of truth.
|
|
86
|
-
var REQUEST_ID_BYTES = C.BYTES.bytes(8);
|
|
87
|
-
|
|
88
|
-
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
|
|
89
|
-
var LEVEL_NAMES = Object.keys(LEVELS);
|
|
90
|
-
|
|
91
|
-
class LogError extends FrameworkError {
|
|
92
|
-
constructor(code, message) {
|
|
93
|
-
super(message, code);
|
|
94
|
-
this.name = "LogError";
|
|
95
|
-
this.permanent = true;
|
|
96
|
-
this.isLogError = true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Single ALS shared across all log instances so request-id propagates
|
|
101
|
-
// regardless of which instance emitted the line. Keyed map so
|
|
102
|
-
// operators can attach more than just requestId (e.g. tenantId).
|
|
103
|
-
var _als = new AsyncLocalStorage();
|
|
104
|
-
|
|
105
|
-
function _getStore() { return _als.getStore() || null; }
|
|
106
|
-
|
|
107
|
-
function _normalizeDestination(d, fallback) {
|
|
108
|
-
if (d === "stdout") return process.stdout;
|
|
109
|
-
if (d === "stderr") return process.stderr;
|
|
110
|
-
if (d && typeof d.write === "function") return d;
|
|
111
|
-
if (typeof d === "function") return { write: d };
|
|
112
|
-
if (d === undefined || d === null) return fallback;
|
|
113
|
-
throw new LogError("log/bad-destination",
|
|
114
|
-
"destination must be 'stdout', 'stderr', a stream with .write, or a function");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function _normalizeLevel(level) {
|
|
118
|
-
if (typeof level === "number") {
|
|
119
|
-
if (level < 0 || level > 4 || !Number.isFinite(level)) {
|
|
120
|
-
throw new LogError("log/bad-level", "numeric level must be 0-4");
|
|
121
|
-
}
|
|
122
|
-
return level;
|
|
123
|
-
}
|
|
124
|
-
if (typeof level === "string") {
|
|
125
|
-
if (LEVELS[level] === undefined) {
|
|
126
|
-
throw new LogError("log/bad-level",
|
|
127
|
-
"level must be one of " + LEVEL_NAMES.join(", "));
|
|
128
|
-
}
|
|
129
|
-
return LEVELS[level];
|
|
130
|
-
}
|
|
131
|
-
throw new LogError("log/bad-level", "level must be a string or number");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
var _CORE_FIELDS = ["timestamp", "level", "message", "requestId"];
|
|
135
|
-
|
|
136
|
-
function _mergeExtras(into, extras, redactExtras) {
|
|
137
|
-
if (!extras || typeof extras !== "object") return false;
|
|
138
|
-
var src = redactExtras ? redact.redact(extras) : extras;
|
|
139
|
-
var keys = Object.keys(src);
|
|
140
|
-
var clobberAttempt = false;
|
|
141
|
-
for (var i = 0; i < keys.length; i++) {
|
|
142
|
-
var k = keys[i];
|
|
143
|
-
if (_CORE_FIELDS.indexOf(k) !== -1) {
|
|
144
|
-
// Operator tried to overwrite a core field — preserve the core
|
|
145
|
-
// value but flag it so misconfig surfaces in the line.
|
|
146
|
-
clobberAttempt = true;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
into[k] = src[k];
|
|
150
|
-
}
|
|
151
|
-
return clobberAttempt;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function _resolveSinks(opts) {
|
|
155
|
-
// Three input shapes — pick exactly one:
|
|
156
|
-
// (a) opts.sinks: [{ stream, level }, ...]
|
|
157
|
-
// (b) opts.destination + opts.errorDestination (legacy two-sink split)
|
|
158
|
-
// (c) neither — defaults to stdout for info-and-below, stderr for warn-and-up
|
|
159
|
-
if (Array.isArray(opts.sinks)) {
|
|
160
|
-
if (opts.destination !== undefined || opts.errorDestination !== undefined) {
|
|
161
|
-
throw new LogError("log/conflicting-sinks",
|
|
162
|
-
"log.create: pass either { sinks: [...] } OR { destination, errorDestination }, not both");
|
|
163
|
-
}
|
|
164
|
-
if (opts.sinks.length === 0) {
|
|
165
|
-
throw new LogError("log/no-sinks",
|
|
166
|
-
"log.create: sinks: [] would silently drop every line — pass at least one sink");
|
|
167
|
-
}
|
|
168
|
-
return opts.sinks.map(function (s, i) {
|
|
169
|
-
if (!s || typeof s !== "object") {
|
|
170
|
-
throw new LogError("log/bad-sink", "sinks[" + i + "]: expected object with { stream, level? }");
|
|
171
|
-
}
|
|
172
|
-
var allowed = ["stream", "level"];
|
|
173
|
-
var keys = Object.keys(s);
|
|
174
|
-
for (var j = 0; j < keys.length; j++) {
|
|
175
|
-
if (allowed.indexOf(keys[j]) === -1) {
|
|
176
|
-
throw new LogError("log/bad-sink",
|
|
177
|
-
"sinks[" + i + "]: unknown key '" + keys[j] + "' (allowed: " + allowed.join(", ") + ")");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
var stream = _normalizeDestination(s.stream, null);
|
|
181
|
-
if (!stream) {
|
|
182
|
-
throw new LogError("log/bad-sink", "sinks[" + i + "]: stream is required");
|
|
183
|
-
}
|
|
184
|
-
// Per-sink level: missing → no filter beyond the global; present → must be valid.
|
|
185
|
-
var minLevel = (s.level === undefined) ? null : _normalizeLevel(s.level);
|
|
186
|
-
return { stream: stream, minLevel: minLevel };
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
// Legacy / default — synthesize the two-sink split.
|
|
190
|
-
var stdoutDest = _normalizeDestination(opts.destination, process.stdout);
|
|
191
|
-
var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
|
|
192
|
-
return [
|
|
193
|
-
// Order matters for emit fan-out: stdout sink catches debug-info-warn;
|
|
194
|
-
// stderr catches error-and-up. Existing behavior — same boundary.
|
|
195
|
-
{ stream: stdoutDest, minLevel: null, _maxLevelExclusive: LEVELS.error },
|
|
196
|
-
{ stream: stderrDest, minLevel: LEVELS.error },
|
|
197
|
-
];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function create(opts) {
|
|
201
|
-
opts = opts || {};
|
|
202
|
-
validateOpts(opts, [
|
|
203
|
-
"level", "destination", "errorDestination", "sinks",
|
|
204
|
-
"format", "redact", "base", "clock",
|
|
205
|
-
], "b.log");
|
|
206
|
-
|
|
207
|
-
// Resolve initial level: env > opts > default
|
|
208
|
-
// safeEnv enforces the size cap + missing/empty handling; LEVELS lookup
|
|
209
|
-
// gates an unrecognised value (silently falls through to opts/default
|
|
210
|
-
// rather than throwing — bad LOG_LEVEL on the env should not crash boot).
|
|
211
|
-
var envLevel = safeEnv().readVar("LOG_LEVEL");
|
|
212
|
-
var level;
|
|
213
|
-
if (envLevel && LEVELS[envLevel] !== undefined) {
|
|
214
|
-
level = LEVELS[envLevel];
|
|
215
|
-
} else if (opts.level !== undefined) {
|
|
216
|
-
level = _normalizeLevel(opts.level);
|
|
217
|
-
} else {
|
|
218
|
-
level = LEVELS.info;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
var sinks = _resolveSinks(opts);
|
|
222
|
-
|
|
223
|
-
var format = opts.format || "json"; // reserved for future formats
|
|
224
|
-
if (format !== "json") {
|
|
225
|
-
throw new LogError("log/bad-format",
|
|
226
|
-
"only 'json' format is supported (got '" + format + "')");
|
|
227
|
-
}
|
|
228
|
-
var redactOn = opts.redact !== false;
|
|
229
|
-
var base = opts.base ? Object.assign({}, opts.base) : {};
|
|
230
|
-
|
|
231
|
-
// Clock injection lets tests pin timestamps deterministically.
|
|
232
|
-
var clock = typeof opts.clock === "function" ? opts.clock : function () { return new Date(); };
|
|
233
|
-
|
|
234
|
-
function _emit(levelName, message, extras, boundChain) {
|
|
235
|
-
if (LEVELS[levelName] < level) return;
|
|
236
|
-
|
|
237
|
-
var entry = {};
|
|
238
|
-
entry.timestamp = clock().toISOString();
|
|
239
|
-
entry.level = levelName;
|
|
240
|
-
entry.message = typeof message === "string" ? message : String(message);
|
|
241
|
-
|
|
242
|
-
// Merge base, then each ancestor's bound context (root → leaf)
|
|
243
|
-
Object.assign(entry, base);
|
|
244
|
-
if (boundChain) {
|
|
245
|
-
for (var i = 0; i < boundChain.length; i++) Object.assign(entry, boundChain[i]);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Request id from ALS — overrides only if not already set by base/bound
|
|
249
|
-
var store = _getStore();
|
|
250
|
-
if (store && store.requestId && entry.requestId === undefined) {
|
|
251
|
-
entry.requestId = store.requestId;
|
|
252
|
-
}
|
|
253
|
-
// Merge any other ALS-bound fields (operator may have set tenantId etc.)
|
|
254
|
-
if (store && store._extra) {
|
|
255
|
-
var ekeys = Object.keys(store._extra);
|
|
256
|
-
for (var j = 0; j < ekeys.length; j++) {
|
|
257
|
-
var ek = ekeys[j];
|
|
258
|
-
if (entry[ek] === undefined) entry[ek] = store._extra[ek];
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Re-stamp core fields — entries from base/bound context cannot
|
|
263
|
-
// overwrite timestamp/level/message
|
|
264
|
-
entry.timestamp = clock().toISOString();
|
|
265
|
-
entry.level = levelName;
|
|
266
|
-
entry.message = typeof message === "string" ? message : String(message);
|
|
267
|
-
|
|
268
|
-
var clobbered = _mergeExtras(entry, extras, redactOn);
|
|
269
|
-
if (clobbered) entry._overwriteAttempt = true;
|
|
270
|
-
|
|
271
|
-
var line;
|
|
272
|
-
try { line = JSON.stringify(entry) + "\n"; }
|
|
273
|
-
catch (_e) {
|
|
274
|
-
// Circular ref or non-serializable extra — emit a fallback line.
|
|
275
|
-
line = JSON.stringify({
|
|
276
|
-
timestamp: entry.timestamp,
|
|
277
|
-
level: levelName,
|
|
278
|
-
message: entry.message,
|
|
279
|
-
_logError: "extras not serializable",
|
|
280
|
-
}) + "\n";
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
var lvlNum = LEVELS[levelName];
|
|
284
|
-
for (var s = 0; s < sinks.length; s++) {
|
|
285
|
-
var sink = sinks[s];
|
|
286
|
-
if (sink.minLevel !== null && lvlNum < sink.minLevel) continue;
|
|
287
|
-
// Legacy default-sinks split uses an exclusive upper bound so the
|
|
288
|
-
// stdout sink catches only info-and-below (warn+ goes to stderr).
|
|
289
|
-
if (sink._maxLevelExclusive !== undefined && lvlNum >= sink._maxLevelExclusive) continue;
|
|
290
|
-
try { sink.stream.write(line); }
|
|
291
|
-
catch (_e) { /* sink write best-effort — never throw out of a log call */ }
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function _makeInstance(boundChain) {
|
|
296
|
-
function child(extra) {
|
|
297
|
-
if (!extra || typeof extra !== "object") {
|
|
298
|
-
throw new LogError("log/bad-bind", "bind(extra) requires an object");
|
|
299
|
-
}
|
|
300
|
-
// Preserve frozen ancestor chain; append a copy so callers can
|
|
301
|
-
// mutate their original without affecting the bound logger.
|
|
302
|
-
var nextChain = boundChain.concat([Object.assign({}, extra)]);
|
|
303
|
-
return _makeInstance(nextChain);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function level_in(name) { return LEVELS[name] !== undefined && LEVELS[name] >= level; }
|
|
307
|
-
function setLevel(l) { level = _normalizeLevel(l); }
|
|
308
|
-
function getLevel() { return LEVEL_NAMES[level]; }
|
|
309
|
-
|
|
310
|
-
function debug(msg, extra) { _emit("debug", msg, extra, boundChain); }
|
|
311
|
-
function info(msg, extra) { _emit("info", msg, extra, boundChain); }
|
|
312
|
-
function warn(msg, extra) { _emit("warn", msg, extra, boundChain); }
|
|
313
|
-
function error(msg, extra) { _emit("error", msg, extra, boundChain); }
|
|
314
|
-
function fatal(msg, extra) { _emit("fatal", msg, extra, boundChain); }
|
|
315
|
-
|
|
316
|
-
function runWithRequestId(id, fn) {
|
|
317
|
-
var store = { requestId: id || null, _extra: {} };
|
|
318
|
-
return _als.run(store, fn);
|
|
319
|
-
}
|
|
320
|
-
function runWithContext(ctx, fn) {
|
|
321
|
-
var existing = _getStore();
|
|
322
|
-
var rid = (ctx && ctx.requestId) || (existing && existing.requestId) || null;
|
|
323
|
-
var extra = Object.assign({},
|
|
324
|
-
existing && existing._extra ? existing._extra : {},
|
|
325
|
-
ctx || {});
|
|
326
|
-
delete extra.requestId;
|
|
327
|
-
return _als.run({ requestId: rid, _extra: extra }, fn);
|
|
328
|
-
}
|
|
329
|
-
function getRequestId() {
|
|
330
|
-
var s = _getStore();
|
|
331
|
-
return s ? s.requestId : null;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function middleware(mwOpts) {
|
|
335
|
-
mwOpts = mwOpts || {};
|
|
336
|
-
var headerName = (mwOpts.headerName || "x-request-id").toLowerCase();
|
|
337
|
-
var setOnRes = mwOpts.setHeader !== false;
|
|
338
|
-
var generate = typeof mwOpts.generate === "function"
|
|
339
|
-
? mwOpts.generate
|
|
340
|
-
: function () {
|
|
341
|
-
// 16 random hex chars — short, sufficient correlation entropy.
|
|
342
|
-
// Routes through the framework token primitive so the entropy
|
|
343
|
-
// source matches the rest of the codebase.
|
|
344
|
-
return crypto().generateToken(REQUEST_ID_BYTES);
|
|
345
|
-
};
|
|
346
|
-
return function logRequestIdMiddleware(req, res, next) {
|
|
347
|
-
var inbound = req.headers && req.headers[headerName];
|
|
348
|
-
var id = (typeof inbound === "string" && inbound.length > 0 && inbound.length <= 200)
|
|
349
|
-
? inbound
|
|
350
|
-
: generate();
|
|
351
|
-
// Strip CRLF defensively before reflecting back into a header
|
|
352
|
-
id = safeBuffer.stripCrlf(String(id));
|
|
353
|
-
req.id = id;
|
|
354
|
-
if (setOnRes && typeof res.setHeader === "function") {
|
|
355
|
-
try { res.setHeader("X-Request-Id", id); } catch (_e) { /* header may be locked */ }
|
|
356
|
-
}
|
|
357
|
-
runWithRequestId(id, function () { next(); });
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
debug: debug,
|
|
363
|
-
info: info,
|
|
364
|
-
warn: warn,
|
|
365
|
-
error: error,
|
|
366
|
-
fatal: fatal,
|
|
367
|
-
bind: child,
|
|
368
|
-
setLevel: setLevel,
|
|
369
|
-
getLevel: getLevel,
|
|
370
|
-
isLevelEnabled: level_in,
|
|
371
|
-
runWithRequestId: runWithRequestId,
|
|
372
|
-
runWithContext: runWithContext,
|
|
373
|
-
getRequestId: getRequestId,
|
|
374
|
-
middleware: middleware,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return _makeInstance([]);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ---- Boot logger ----
|
|
382
|
-
//
|
|
383
|
-
// Framework-internal modules emit human-readable startup chatter
|
|
384
|
-
// during boot ("[blamejs:db] ready", "[blamejs:vault] WARNING: …"),
|
|
385
|
-
// distinct from the structured app-level logger above. The boot
|
|
386
|
-
// channel is TTY-aware:
|
|
387
|
-
//
|
|
388
|
-
// - stdout is a TTY → "[blamejs:<name>] <message>" line
|
|
389
|
-
// - stdout is piped → JSON line { timestamp, level, message,
|
|
390
|
-
// component: <name>, boot: true }
|
|
391
|
-
//
|
|
392
|
-
// This keeps `npm start` readable for humans while letting log
|
|
393
|
-
// aggregators ingest boot chatter as structured records.
|
|
394
|
-
//
|
|
395
|
-
// Returned object is a callable (info path) plus .info / .warn /
|
|
396
|
-
// .error / .prefix members so calls like `log("ready")` and
|
|
397
|
-
// `log.warn("…")` both work.
|
|
398
|
-
function boot(name) {
|
|
399
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
400
|
-
throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
|
|
401
|
-
}
|
|
402
|
-
var prefix = "[blamejs:" + name + "] ";
|
|
403
|
-
|
|
404
|
-
function _emit(levelName, msg) {
|
|
405
|
-
// Route through console.{log,error} (not process.{stdout,stderr}.write)
|
|
406
|
-
// so test runners that capture console output behave as operators
|
|
407
|
-
// expect — same rationale as the original lib/logger.js.
|
|
408
|
-
var sink = (LEVELS[levelName] >= LEVELS.warn) ? console.error : console.log;
|
|
409
|
-
var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
|
|
410
|
-
var isTty = !!(stream && stream.isTTY);
|
|
411
|
-
if (isTty) {
|
|
412
|
-
sink(prefix + String(msg));
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
var entry = {
|
|
416
|
-
timestamp: new Date().toISOString(),
|
|
417
|
-
level: levelName,
|
|
418
|
-
message: String(msg),
|
|
419
|
-
component: name,
|
|
420
|
-
boot: true,
|
|
421
|
-
};
|
|
422
|
-
sink(JSON.stringify(entry));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function debug(msg, fields) {
|
|
426
|
-
// Boot-time debug entries route through console.log unless LOG_LEVEL
|
|
427
|
-
// suppresses them. fields (when present) get JSON-appended so
|
|
428
|
-
// operators see the structured context.
|
|
429
|
-
if (LEVELS.debug < _bootMinLevel()) return;
|
|
430
|
-
var rendered = msg;
|
|
431
|
-
if (fields !== undefined) {
|
|
432
|
-
try { rendered = msg + " " + JSON.stringify(fields); }
|
|
433
|
-
catch (_e) { rendered = msg; }
|
|
434
|
-
}
|
|
435
|
-
_emit("debug", rendered);
|
|
436
|
-
}
|
|
437
|
-
function info(msg) { _emit("info", msg); }
|
|
438
|
-
function warn(msg) { _emit("warn", msg); }
|
|
439
|
-
function error(msg) { _emit("error", msg); }
|
|
440
|
-
|
|
441
|
-
// The returned function is the info path so `log(msg)` matches the
|
|
442
|
-
// existing call shape across the codebase.
|
|
443
|
-
info.debug = debug;
|
|
444
|
-
info.info = info;
|
|
445
|
-
info.warn = warn;
|
|
446
|
-
info.error = error;
|
|
447
|
-
info.prefix = prefix;
|
|
448
|
-
return info;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// makeViaOrFallback — closure factory for operator-log routing. Used by
|
|
452
|
-
// bundler / dev / error-page / pqc-gate (and similar primitives) that
|
|
453
|
-
// accept opts.log but must keep emitting through a per-module fallback
|
|
454
|
-
// when the operator didn't pass one. Replaces the per-file
|
|
455
|
-
// `function _logVia(log, level, message, fields) { if (log && typeof
|
|
456
|
-
// log[level] === "function") { try { log[level](message, fields); }
|
|
457
|
-
// catch ... } return; } ... fallback;` boilerplate.
|
|
458
|
-
//
|
|
459
|
-
// var _logVia = log.makeViaOrFallback(opts.log, log.boot("bundler"));
|
|
460
|
-
// _logVia("error", "build-failed", { reason: "..." });
|
|
461
|
-
//
|
|
462
|
-
// The operator log call is best-effort: a misbehaving log[level] swallows
|
|
463
|
-
// internally rather than crash the caller. Fallback is invoked only when
|
|
464
|
-
// the operator log is absent or doesn't expose the requested level.
|
|
465
|
-
function makeViaOrFallback(operatorLog, fallbackLog) {
|
|
466
|
-
return function (level, message, fields) {
|
|
467
|
-
if (operatorLog && typeof operatorLog[level] === "function") {
|
|
468
|
-
try { operatorLog[level](message, fields); }
|
|
469
|
-
catch (_e) { /* operator log best-effort */ }
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
var line = message + (fields ? " " + JSON.stringify(fields) : "");
|
|
473
|
-
var fb = (level === "error" || level === "fatal") ? fallbackLog.error
|
|
474
|
-
: (level === "warn") ? fallbackLog.warn
|
|
475
|
-
: fallbackLog.info;
|
|
476
|
-
if (typeof fb === "function") fb(line);
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Boot-time minimum level (debug suppressed unless explicitly enabled).
|
|
481
|
-
// Uses raw process.env per the documented load-cycle exception: log.js
|
|
482
|
-
// runs before safeEnv on the boot path; safeEnv requires log, so log
|
|
483
|
-
// can't go through safeEnv to read its own level.
|
|
484
|
-
function _bootMinLevel() {
|
|
485
|
-
// allow:raw-process-env — see header comment above
|
|
486
|
-
var raw = process.env.BLAMEJS_BOOT_LOG_LEVEL || process.env.LOG_LEVEL || "info";
|
|
487
|
-
return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
module.exports = {
|
|
491
|
-
create: create,
|
|
492
|
-
boot: boot,
|
|
493
|
-
makeViaOrFallback: makeViaOrFallback,
|
|
494
|
-
LEVELS: LEVELS,
|
|
495
|
-
LogError: LogError,
|
|
496
|
-
// Module-level helpers for code paths that don't have a logger
|
|
497
|
-
// instance handy but still need to read ALS state.
|
|
498
|
-
getRequestId: function () { var s = _getStore(); return s ? s.requestId : null; },
|
|
499
|
-
runWithRequestId: function (id, fn) { return _als.run({ requestId: id || null, _extra: {} }, fn); },
|
|
500
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* log — structured JSON application logger with request-id correlation.
|
|
4
|
+
*
|
|
5
|
+
* Distinct concern from lib/logger.js: logger.js is the framework's
|
|
6
|
+
* own boot/operational chatter to console with `[blamejs:<name>] `
|
|
7
|
+
* prefix (humans watching `npm start`). lib/log.js is the app-level
|
|
8
|
+
* structured logger meant to be ingested by a log aggregator.
|
|
9
|
+
*
|
|
10
|
+
* Each line is one JSON object on a single line, terminated with `\n`.
|
|
11
|
+
* Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4).
|
|
12
|
+
* Default routing: debug / info / warn → stdout; error / fatal → stderr.
|
|
13
|
+
* Multi-sink config (`sinks: [...]`) takes full control of routing.
|
|
14
|
+
*
|
|
15
|
+
* var log = b.log.create({
|
|
16
|
+
* level: "info", // env LOG_LEVEL > opts.level > "info"
|
|
17
|
+
* base: { service: "myapp", version: "1.2.3" },
|
|
18
|
+
* redact: true, // run extras through lib/redact
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Multi-sink: each sink gets every line at-or-above its own level.
|
|
22
|
+
* // Default (no `sinks` opt) splits info-and-below to stdout and
|
|
23
|
+
* // warn-and-up to stderr — same as before.
|
|
24
|
+
* var log = b.log.create({
|
|
25
|
+
* level: "debug",
|
|
26
|
+
* sinks: [
|
|
27
|
+
* { stream: process.stdout, level: "info" },
|
|
28
|
+
* { stream: fs.createWriteStream("./logs/debug.log"), level: "debug" },
|
|
29
|
+
* { stream: fs.createWriteStream("./logs/errors.log"), level: "error" },
|
|
30
|
+
* ],
|
|
31
|
+
* });
|
|
32
|
+
* // sinks: [...] is mutually exclusive with destination/errorDestination.
|
|
33
|
+
*
|
|
34
|
+
* log.info("user logged in", { userId: "u-1" });
|
|
35
|
+
* log.error("payment failed", { orderId, err: e.message });
|
|
36
|
+
*
|
|
37
|
+
* // Child with bound context
|
|
38
|
+
* var authLog = log.bind({ component: "auth" });
|
|
39
|
+
* authLog.info("password verified", { userId: "u-1" });
|
|
40
|
+
*
|
|
41
|
+
* // Request correlation via AsyncLocalStorage (Node async context)
|
|
42
|
+
* await log.runWithRequestId("req-abc", async function () {
|
|
43
|
+
* log.info("inside request"); // → ..., "requestId": "req-abc"
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Router middleware that allocates a requestId and binds it for
|
|
47
|
+
* // the entire request async chain
|
|
48
|
+
* r.use(log.middleware());
|
|
49
|
+
*
|
|
50
|
+
* Field merge order (last wins):
|
|
51
|
+
* 1. base context from create()
|
|
52
|
+
* 2. bound context from bind() (each ancestor up the chain)
|
|
53
|
+
* 3. requestId from ALS (if set)
|
|
54
|
+
* 4. extra arg from .info(msg, extra)
|
|
55
|
+
* 5. core fields: timestamp, level, message
|
|
56
|
+
*
|
|
57
|
+
* Core fields cannot be overwritten by extras — log.info("hi", { level: "X" })
|
|
58
|
+
* keeps level: "info" in the emitted line, with an _overwriteAttempt
|
|
59
|
+
* flag if the operator tried to clobber.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
var { AsyncLocalStorage } = require("node:async_hooks");
|
|
63
|
+
var C = require("./constants");
|
|
64
|
+
var lazyRequire = require("./lazy-require");
|
|
65
|
+
var redact = require("./redact");
|
|
66
|
+
var safeBuffer = require("./safe-buffer");
|
|
67
|
+
var validateOpts = require("./validate-opts");
|
|
68
|
+
var { FrameworkError } = require("./framework-error");
|
|
69
|
+
|
|
70
|
+
// Lazy resolves to break load-order cycles:
|
|
71
|
+
// - parsers/safe-env requires log (boot logger for env-read errors).
|
|
72
|
+
// log can't require safe-env at top because safe-env's top-level
|
|
73
|
+
// `var log = boot("env")` would see log's exports half-built.
|
|
74
|
+
// - crypto requires constants + framework-error + vendor; it does NOT
|
|
75
|
+
// require log, so a top-of-file require would technically work, but
|
|
76
|
+
// keeping crypto lazy keeps log loadable from anywhere without
|
|
77
|
+
// pulling the whole crypto bundle into the framework's earliest
|
|
78
|
+
// boot path (request-id middleware needs only generateToken).
|
|
79
|
+
var safeEnv = lazyRequire(function () { return require("./parsers/safe-env"); });
|
|
80
|
+
var crypto = lazyRequire(function () { return require("./crypto"); });
|
|
81
|
+
|
|
82
|
+
// Request-id correlation token — 8 bytes hex-encoded (16 chars). Short
|
|
83
|
+
// enough to read in a log line, long enough to keep collisions far below
|
|
84
|
+
// audible-noise even at 1M req/s sustained. Routed through C.BYTES so
|
|
85
|
+
// the file's byte arithmetic has a single source of truth.
|
|
86
|
+
var REQUEST_ID_BYTES = C.BYTES.bytes(8);
|
|
87
|
+
|
|
88
|
+
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 };
|
|
89
|
+
var LEVEL_NAMES = Object.keys(LEVELS);
|
|
90
|
+
|
|
91
|
+
class LogError extends FrameworkError {
|
|
92
|
+
constructor(code, message) {
|
|
93
|
+
super(message, code);
|
|
94
|
+
this.name = "LogError";
|
|
95
|
+
this.permanent = true;
|
|
96
|
+
this.isLogError = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Single ALS shared across all log instances so request-id propagates
|
|
101
|
+
// regardless of which instance emitted the line. Keyed map so
|
|
102
|
+
// operators can attach more than just requestId (e.g. tenantId).
|
|
103
|
+
var _als = new AsyncLocalStorage();
|
|
104
|
+
|
|
105
|
+
function _getStore() { return _als.getStore() || null; }
|
|
106
|
+
|
|
107
|
+
function _normalizeDestination(d, fallback) {
|
|
108
|
+
if (d === "stdout") return process.stdout;
|
|
109
|
+
if (d === "stderr") return process.stderr;
|
|
110
|
+
if (d && typeof d.write === "function") return d;
|
|
111
|
+
if (typeof d === "function") return { write: d };
|
|
112
|
+
if (d === undefined || d === null) return fallback;
|
|
113
|
+
throw new LogError("log/bad-destination",
|
|
114
|
+
"destination must be 'stdout', 'stderr', a stream with .write, or a function");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _normalizeLevel(level) {
|
|
118
|
+
if (typeof level === "number") {
|
|
119
|
+
if (level < 0 || level > 4 || !Number.isFinite(level)) {
|
|
120
|
+
throw new LogError("log/bad-level", "numeric level must be 0-4");
|
|
121
|
+
}
|
|
122
|
+
return level;
|
|
123
|
+
}
|
|
124
|
+
if (typeof level === "string") {
|
|
125
|
+
if (LEVELS[level] === undefined) {
|
|
126
|
+
throw new LogError("log/bad-level",
|
|
127
|
+
"level must be one of " + LEVEL_NAMES.join(", "));
|
|
128
|
+
}
|
|
129
|
+
return LEVELS[level];
|
|
130
|
+
}
|
|
131
|
+
throw new LogError("log/bad-level", "level must be a string or number");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
var _CORE_FIELDS = ["timestamp", "level", "message", "requestId"];
|
|
135
|
+
|
|
136
|
+
function _mergeExtras(into, extras, redactExtras) {
|
|
137
|
+
if (!extras || typeof extras !== "object") return false;
|
|
138
|
+
var src = redactExtras ? redact.redact(extras) : extras;
|
|
139
|
+
var keys = Object.keys(src);
|
|
140
|
+
var clobberAttempt = false;
|
|
141
|
+
for (var i = 0; i < keys.length; i++) {
|
|
142
|
+
var k = keys[i];
|
|
143
|
+
if (_CORE_FIELDS.indexOf(k) !== -1) {
|
|
144
|
+
// Operator tried to overwrite a core field — preserve the core
|
|
145
|
+
// value but flag it so misconfig surfaces in the line.
|
|
146
|
+
clobberAttempt = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
into[k] = src[k];
|
|
150
|
+
}
|
|
151
|
+
return clobberAttempt;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _resolveSinks(opts) {
|
|
155
|
+
// Three input shapes — pick exactly one:
|
|
156
|
+
// (a) opts.sinks: [{ stream, level }, ...]
|
|
157
|
+
// (b) opts.destination + opts.errorDestination (legacy two-sink split)
|
|
158
|
+
// (c) neither — defaults to stdout for info-and-below, stderr for warn-and-up
|
|
159
|
+
if (Array.isArray(opts.sinks)) {
|
|
160
|
+
if (opts.destination !== undefined || opts.errorDestination !== undefined) {
|
|
161
|
+
throw new LogError("log/conflicting-sinks",
|
|
162
|
+
"log.create: pass either { sinks: [...] } OR { destination, errorDestination }, not both");
|
|
163
|
+
}
|
|
164
|
+
if (opts.sinks.length === 0) {
|
|
165
|
+
throw new LogError("log/no-sinks",
|
|
166
|
+
"log.create: sinks: [] would silently drop every line — pass at least one sink");
|
|
167
|
+
}
|
|
168
|
+
return opts.sinks.map(function (s, i) {
|
|
169
|
+
if (!s || typeof s !== "object") {
|
|
170
|
+
throw new LogError("log/bad-sink", "sinks[" + i + "]: expected object with { stream, level? }");
|
|
171
|
+
}
|
|
172
|
+
var allowed = ["stream", "level"];
|
|
173
|
+
var keys = Object.keys(s);
|
|
174
|
+
for (var j = 0; j < keys.length; j++) {
|
|
175
|
+
if (allowed.indexOf(keys[j]) === -1) {
|
|
176
|
+
throw new LogError("log/bad-sink",
|
|
177
|
+
"sinks[" + i + "]: unknown key '" + keys[j] + "' (allowed: " + allowed.join(", ") + ")");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
var stream = _normalizeDestination(s.stream, null);
|
|
181
|
+
if (!stream) {
|
|
182
|
+
throw new LogError("log/bad-sink", "sinks[" + i + "]: stream is required");
|
|
183
|
+
}
|
|
184
|
+
// Per-sink level: missing → no filter beyond the global; present → must be valid.
|
|
185
|
+
var minLevel = (s.level === undefined) ? null : _normalizeLevel(s.level);
|
|
186
|
+
return { stream: stream, minLevel: minLevel };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Legacy / default — synthesize the two-sink split.
|
|
190
|
+
var stdoutDest = _normalizeDestination(opts.destination, process.stdout);
|
|
191
|
+
var stderrDest = _normalizeDestination(opts.errorDestination, process.stderr);
|
|
192
|
+
return [
|
|
193
|
+
// Order matters for emit fan-out: stdout sink catches debug-info-warn;
|
|
194
|
+
// stderr catches error-and-up. Existing behavior — same boundary.
|
|
195
|
+
{ stream: stdoutDest, minLevel: null, _maxLevelExclusive: LEVELS.error },
|
|
196
|
+
{ stream: stderrDest, minLevel: LEVELS.error },
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function create(opts) {
|
|
201
|
+
opts = opts || {};
|
|
202
|
+
validateOpts(opts, [
|
|
203
|
+
"level", "destination", "errorDestination", "sinks",
|
|
204
|
+
"format", "redact", "base", "clock",
|
|
205
|
+
], "b.log");
|
|
206
|
+
|
|
207
|
+
// Resolve initial level: env > opts > default
|
|
208
|
+
// safeEnv enforces the size cap + missing/empty handling; LEVELS lookup
|
|
209
|
+
// gates an unrecognised value (silently falls through to opts/default
|
|
210
|
+
// rather than throwing — bad LOG_LEVEL on the env should not crash boot).
|
|
211
|
+
var envLevel = safeEnv().readVar("LOG_LEVEL");
|
|
212
|
+
var level;
|
|
213
|
+
if (envLevel && LEVELS[envLevel] !== undefined) {
|
|
214
|
+
level = LEVELS[envLevel];
|
|
215
|
+
} else if (opts.level !== undefined) {
|
|
216
|
+
level = _normalizeLevel(opts.level);
|
|
217
|
+
} else {
|
|
218
|
+
level = LEVELS.info;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
var sinks = _resolveSinks(opts);
|
|
222
|
+
|
|
223
|
+
var format = opts.format || "json"; // reserved for future formats
|
|
224
|
+
if (format !== "json") {
|
|
225
|
+
throw new LogError("log/bad-format",
|
|
226
|
+
"only 'json' format is supported (got '" + format + "')");
|
|
227
|
+
}
|
|
228
|
+
var redactOn = opts.redact !== false;
|
|
229
|
+
var base = opts.base ? Object.assign({}, opts.base) : {};
|
|
230
|
+
|
|
231
|
+
// Clock injection lets tests pin timestamps deterministically.
|
|
232
|
+
var clock = typeof opts.clock === "function" ? opts.clock : function () { return new Date(); };
|
|
233
|
+
|
|
234
|
+
function _emit(levelName, message, extras, boundChain) {
|
|
235
|
+
if (LEVELS[levelName] < level) return;
|
|
236
|
+
|
|
237
|
+
var entry = {};
|
|
238
|
+
entry.timestamp = clock().toISOString();
|
|
239
|
+
entry.level = levelName;
|
|
240
|
+
entry.message = typeof message === "string" ? message : String(message);
|
|
241
|
+
|
|
242
|
+
// Merge base, then each ancestor's bound context (root → leaf)
|
|
243
|
+
Object.assign(entry, base);
|
|
244
|
+
if (boundChain) {
|
|
245
|
+
for (var i = 0; i < boundChain.length; i++) Object.assign(entry, boundChain[i]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Request id from ALS — overrides only if not already set by base/bound
|
|
249
|
+
var store = _getStore();
|
|
250
|
+
if (store && store.requestId && entry.requestId === undefined) {
|
|
251
|
+
entry.requestId = store.requestId;
|
|
252
|
+
}
|
|
253
|
+
// Merge any other ALS-bound fields (operator may have set tenantId etc.)
|
|
254
|
+
if (store && store._extra) {
|
|
255
|
+
var ekeys = Object.keys(store._extra);
|
|
256
|
+
for (var j = 0; j < ekeys.length; j++) {
|
|
257
|
+
var ek = ekeys[j];
|
|
258
|
+
if (entry[ek] === undefined) entry[ek] = store._extra[ek];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Re-stamp core fields — entries from base/bound context cannot
|
|
263
|
+
// overwrite timestamp/level/message
|
|
264
|
+
entry.timestamp = clock().toISOString();
|
|
265
|
+
entry.level = levelName;
|
|
266
|
+
entry.message = typeof message === "string" ? message : String(message);
|
|
267
|
+
|
|
268
|
+
var clobbered = _mergeExtras(entry, extras, redactOn);
|
|
269
|
+
if (clobbered) entry._overwriteAttempt = true;
|
|
270
|
+
|
|
271
|
+
var line;
|
|
272
|
+
try { line = JSON.stringify(entry) + "\n"; }
|
|
273
|
+
catch (_e) {
|
|
274
|
+
// Circular ref or non-serializable extra — emit a fallback line.
|
|
275
|
+
line = JSON.stringify({
|
|
276
|
+
timestamp: entry.timestamp,
|
|
277
|
+
level: levelName,
|
|
278
|
+
message: entry.message,
|
|
279
|
+
_logError: "extras not serializable",
|
|
280
|
+
}) + "\n";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var lvlNum = LEVELS[levelName];
|
|
284
|
+
for (var s = 0; s < sinks.length; s++) {
|
|
285
|
+
var sink = sinks[s];
|
|
286
|
+
if (sink.minLevel !== null && lvlNum < sink.minLevel) continue;
|
|
287
|
+
// Legacy default-sinks split uses an exclusive upper bound so the
|
|
288
|
+
// stdout sink catches only info-and-below (warn+ goes to stderr).
|
|
289
|
+
if (sink._maxLevelExclusive !== undefined && lvlNum >= sink._maxLevelExclusive) continue;
|
|
290
|
+
try { sink.stream.write(line); }
|
|
291
|
+
catch (_e) { /* sink write best-effort — never throw out of a log call */ }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function _makeInstance(boundChain) {
|
|
296
|
+
function child(extra) {
|
|
297
|
+
if (!extra || typeof extra !== "object") {
|
|
298
|
+
throw new LogError("log/bad-bind", "bind(extra) requires an object");
|
|
299
|
+
}
|
|
300
|
+
// Preserve frozen ancestor chain; append a copy so callers can
|
|
301
|
+
// mutate their original without affecting the bound logger.
|
|
302
|
+
var nextChain = boundChain.concat([Object.assign({}, extra)]);
|
|
303
|
+
return _makeInstance(nextChain);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function level_in(name) { return LEVELS[name] !== undefined && LEVELS[name] >= level; }
|
|
307
|
+
function setLevel(l) { level = _normalizeLevel(l); }
|
|
308
|
+
function getLevel() { return LEVEL_NAMES[level]; }
|
|
309
|
+
|
|
310
|
+
function debug(msg, extra) { _emit("debug", msg, extra, boundChain); }
|
|
311
|
+
function info(msg, extra) { _emit("info", msg, extra, boundChain); }
|
|
312
|
+
function warn(msg, extra) { _emit("warn", msg, extra, boundChain); }
|
|
313
|
+
function error(msg, extra) { _emit("error", msg, extra, boundChain); }
|
|
314
|
+
function fatal(msg, extra) { _emit("fatal", msg, extra, boundChain); }
|
|
315
|
+
|
|
316
|
+
function runWithRequestId(id, fn) {
|
|
317
|
+
var store = { requestId: id || null, _extra: {} };
|
|
318
|
+
return _als.run(store, fn);
|
|
319
|
+
}
|
|
320
|
+
function runWithContext(ctx, fn) {
|
|
321
|
+
var existing = _getStore();
|
|
322
|
+
var rid = (ctx && ctx.requestId) || (existing && existing.requestId) || null;
|
|
323
|
+
var extra = Object.assign({},
|
|
324
|
+
existing && existing._extra ? existing._extra : {},
|
|
325
|
+
ctx || {});
|
|
326
|
+
delete extra.requestId;
|
|
327
|
+
return _als.run({ requestId: rid, _extra: extra }, fn);
|
|
328
|
+
}
|
|
329
|
+
function getRequestId() {
|
|
330
|
+
var s = _getStore();
|
|
331
|
+
return s ? s.requestId : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function middleware(mwOpts) {
|
|
335
|
+
mwOpts = mwOpts || {};
|
|
336
|
+
var headerName = (mwOpts.headerName || "x-request-id").toLowerCase();
|
|
337
|
+
var setOnRes = mwOpts.setHeader !== false;
|
|
338
|
+
var generate = typeof mwOpts.generate === "function"
|
|
339
|
+
? mwOpts.generate
|
|
340
|
+
: function () {
|
|
341
|
+
// 16 random hex chars — short, sufficient correlation entropy.
|
|
342
|
+
// Routes through the framework token primitive so the entropy
|
|
343
|
+
// source matches the rest of the codebase.
|
|
344
|
+
return crypto().generateToken(REQUEST_ID_BYTES);
|
|
345
|
+
};
|
|
346
|
+
return function logRequestIdMiddleware(req, res, next) {
|
|
347
|
+
var inbound = req.headers && req.headers[headerName];
|
|
348
|
+
var id = (typeof inbound === "string" && inbound.length > 0 && inbound.length <= 200)
|
|
349
|
+
? inbound
|
|
350
|
+
: generate();
|
|
351
|
+
// Strip CRLF defensively before reflecting back into a header
|
|
352
|
+
id = safeBuffer.stripCrlf(String(id));
|
|
353
|
+
req.id = id;
|
|
354
|
+
if (setOnRes && typeof res.setHeader === "function") {
|
|
355
|
+
try { res.setHeader("X-Request-Id", id); } catch (_e) { /* header may be locked */ }
|
|
356
|
+
}
|
|
357
|
+
runWithRequestId(id, function () { next(); });
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
debug: debug,
|
|
363
|
+
info: info,
|
|
364
|
+
warn: warn,
|
|
365
|
+
error: error,
|
|
366
|
+
fatal: fatal,
|
|
367
|
+
bind: child,
|
|
368
|
+
setLevel: setLevel,
|
|
369
|
+
getLevel: getLevel,
|
|
370
|
+
isLevelEnabled: level_in,
|
|
371
|
+
runWithRequestId: runWithRequestId,
|
|
372
|
+
runWithContext: runWithContext,
|
|
373
|
+
getRequestId: getRequestId,
|
|
374
|
+
middleware: middleware,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return _makeInstance([]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---- Boot logger ----
|
|
382
|
+
//
|
|
383
|
+
// Framework-internal modules emit human-readable startup chatter
|
|
384
|
+
// during boot ("[blamejs:db] ready", "[blamejs:vault] WARNING: …"),
|
|
385
|
+
// distinct from the structured app-level logger above. The boot
|
|
386
|
+
// channel is TTY-aware:
|
|
387
|
+
//
|
|
388
|
+
// - stdout is a TTY → "[blamejs:<name>] <message>" line
|
|
389
|
+
// - stdout is piped → JSON line { timestamp, level, message,
|
|
390
|
+
// component: <name>, boot: true }
|
|
391
|
+
//
|
|
392
|
+
// This keeps `npm start` readable for humans while letting log
|
|
393
|
+
// aggregators ingest boot chatter as structured records.
|
|
394
|
+
//
|
|
395
|
+
// Returned object is a callable (info path) plus .info / .warn /
|
|
396
|
+
// .error / .prefix members so calls like `log("ready")` and
|
|
397
|
+
// `log.warn("…")` both work.
|
|
398
|
+
function boot(name) {
|
|
399
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
400
|
+
throw new LogError("log/bad-name", "log.boot(name) requires a non-empty name");
|
|
401
|
+
}
|
|
402
|
+
var prefix = "[blamejs:" + name + "] ";
|
|
403
|
+
|
|
404
|
+
function _emit(levelName, msg) {
|
|
405
|
+
// Route through console.{log,error} (not process.{stdout,stderr}.write)
|
|
406
|
+
// so test runners that capture console output behave as operators
|
|
407
|
+
// expect — same rationale as the original lib/logger.js.
|
|
408
|
+
var sink = (LEVELS[levelName] >= LEVELS.warn) ? console.error : console.log;
|
|
409
|
+
var stream = (LEVELS[levelName] >= LEVELS.warn) ? process.stderr : process.stdout;
|
|
410
|
+
var isTty = !!(stream && stream.isTTY);
|
|
411
|
+
if (isTty) {
|
|
412
|
+
sink(prefix + String(msg));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
var entry = {
|
|
416
|
+
timestamp: new Date().toISOString(),
|
|
417
|
+
level: levelName,
|
|
418
|
+
message: String(msg),
|
|
419
|
+
component: name,
|
|
420
|
+
boot: true,
|
|
421
|
+
};
|
|
422
|
+
sink(JSON.stringify(entry));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function debug(msg, fields) {
|
|
426
|
+
// Boot-time debug entries route through console.log unless LOG_LEVEL
|
|
427
|
+
// suppresses them. fields (when present) get JSON-appended so
|
|
428
|
+
// operators see the structured context.
|
|
429
|
+
if (LEVELS.debug < _bootMinLevel()) return;
|
|
430
|
+
var rendered = msg;
|
|
431
|
+
if (fields !== undefined) {
|
|
432
|
+
try { rendered = msg + " " + JSON.stringify(fields); }
|
|
433
|
+
catch (_e) { rendered = msg; }
|
|
434
|
+
}
|
|
435
|
+
_emit("debug", rendered);
|
|
436
|
+
}
|
|
437
|
+
function info(msg) { _emit("info", msg); }
|
|
438
|
+
function warn(msg) { _emit("warn", msg); }
|
|
439
|
+
function error(msg) { _emit("error", msg); }
|
|
440
|
+
|
|
441
|
+
// The returned function is the info path so `log(msg)` matches the
|
|
442
|
+
// existing call shape across the codebase.
|
|
443
|
+
info.debug = debug;
|
|
444
|
+
info.info = info;
|
|
445
|
+
info.warn = warn;
|
|
446
|
+
info.error = error;
|
|
447
|
+
info.prefix = prefix;
|
|
448
|
+
return info;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// makeViaOrFallback — closure factory for operator-log routing. Used by
|
|
452
|
+
// bundler / dev / error-page / pqc-gate (and similar primitives) that
|
|
453
|
+
// accept opts.log but must keep emitting through a per-module fallback
|
|
454
|
+
// when the operator didn't pass one. Replaces the per-file
|
|
455
|
+
// `function _logVia(log, level, message, fields) { if (log && typeof
|
|
456
|
+
// log[level] === "function") { try { log[level](message, fields); }
|
|
457
|
+
// catch ... } return; } ... fallback;` boilerplate.
|
|
458
|
+
//
|
|
459
|
+
// var _logVia = log.makeViaOrFallback(opts.log, log.boot("bundler"));
|
|
460
|
+
// _logVia("error", "build-failed", { reason: "..." });
|
|
461
|
+
//
|
|
462
|
+
// The operator log call is best-effort: a misbehaving log[level] swallows
|
|
463
|
+
// internally rather than crash the caller. Fallback is invoked only when
|
|
464
|
+
// the operator log is absent or doesn't expose the requested level.
|
|
465
|
+
function makeViaOrFallback(operatorLog, fallbackLog) {
|
|
466
|
+
return function (level, message, fields) {
|
|
467
|
+
if (operatorLog && typeof operatorLog[level] === "function") {
|
|
468
|
+
try { operatorLog[level](message, fields); }
|
|
469
|
+
catch (_e) { /* operator log best-effort */ }
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
var line = message + (fields ? " " + JSON.stringify(fields) : "");
|
|
473
|
+
var fb = (level === "error" || level === "fatal") ? fallbackLog.error
|
|
474
|
+
: (level === "warn") ? fallbackLog.warn
|
|
475
|
+
: fallbackLog.info;
|
|
476
|
+
if (typeof fb === "function") fb(line);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Boot-time minimum level (debug suppressed unless explicitly enabled).
|
|
481
|
+
// Uses raw process.env per the documented load-cycle exception: log.js
|
|
482
|
+
// runs before safeEnv on the boot path; safeEnv requires log, so log
|
|
483
|
+
// can't go through safeEnv to read its own level.
|
|
484
|
+
function _bootMinLevel() {
|
|
485
|
+
// allow:raw-process-env — see header comment above
|
|
486
|
+
var raw = process.env.BLAMEJS_BOOT_LOG_LEVEL || process.env.LOG_LEVEL || "info";
|
|
487
|
+
return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
module.exports = {
|
|
491
|
+
create: create,
|
|
492
|
+
boot: boot,
|
|
493
|
+
makeViaOrFallback: makeViaOrFallback,
|
|
494
|
+
LEVELS: LEVELS,
|
|
495
|
+
LogError: LogError,
|
|
496
|
+
// Module-level helpers for code paths that don't have a logger
|
|
497
|
+
// instance handy but still need to read ALS state.
|
|
498
|
+
getRequestId: function () { var s = _getStore(); return s ? s.requestId : null; },
|
|
499
|
+
runWithRequestId: function (id, fn) { return _als.run({ requestId: id || null, _extra: {} }, fn); },
|
|
500
|
+
};
|