@ubercode/chronicler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/cli.js +6696 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +769 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +427 -0
- package/dist/index.d.ts +427 -0
- package/dist/index.js +759 -0
- package/dist/index.js.map +1 -0
- package/package.json +112 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
// src/core/constants.ts
|
|
2
|
+
var LOG_LEVELS = {
|
|
3
|
+
fatal: 0,
|
|
4
|
+
// System is unusable
|
|
5
|
+
critical: 1,
|
|
6
|
+
// Critical conditions requiring immediate attention
|
|
7
|
+
alert: 2,
|
|
8
|
+
// Action must be taken immediately
|
|
9
|
+
error: 3,
|
|
10
|
+
// Error conditions
|
|
11
|
+
warn: 4,
|
|
12
|
+
// Warning conditions
|
|
13
|
+
audit: 5,
|
|
14
|
+
// Audit trail events (compliance, security)
|
|
15
|
+
info: 6,
|
|
16
|
+
// Informational messages
|
|
17
|
+
debug: 7,
|
|
18
|
+
// Debug-level messages
|
|
19
|
+
trace: 8
|
|
20
|
+
// Trace-level messages (very verbose)
|
|
21
|
+
};
|
|
22
|
+
var DEFAULT_REQUIRED_LEVELS = Object.keys(LOG_LEVELS);
|
|
23
|
+
var DEFAULT_CORRELATION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
24
|
+
var ROOT_FORK_ID = "0";
|
|
25
|
+
var FORK_ID_SEPARATOR = ".";
|
|
26
|
+
var DEFAULT_MAX_CONTEXT_KEYS = 100;
|
|
27
|
+
var DEFAULT_MAX_FORK_DEPTH = 10;
|
|
28
|
+
var DEFAULT_MAX_ACTIVE_CORRELATIONS = 1e3;
|
|
29
|
+
|
|
30
|
+
// src/core/errors.ts
|
|
31
|
+
var ChroniclerError = class extends Error {
|
|
32
|
+
code;
|
|
33
|
+
/**
|
|
34
|
+
* @param code - Machine-readable error category discriminator
|
|
35
|
+
* @param message - Human-readable description of the error
|
|
36
|
+
*/
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "ChroniclerError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/core/backend.ts
|
|
45
|
+
var CONSOLE_LEVEL_MAP = {
|
|
46
|
+
fatal: "error",
|
|
47
|
+
critical: "error",
|
|
48
|
+
alert: "error",
|
|
49
|
+
error: "error",
|
|
50
|
+
warn: "warn",
|
|
51
|
+
audit: "info",
|
|
52
|
+
info: "info",
|
|
53
|
+
debug: "debug",
|
|
54
|
+
trace: "debug"
|
|
55
|
+
};
|
|
56
|
+
var LEVEL_FALLBACK_CHAINS = {
|
|
57
|
+
fatal: ["critical", "error", "warn", "info"],
|
|
58
|
+
critical: ["error", "warn", "info"],
|
|
59
|
+
alert: ["error", "warn", "info"],
|
|
60
|
+
error: ["warn", "info"],
|
|
61
|
+
warn: ["info"],
|
|
62
|
+
audit: ["info"],
|
|
63
|
+
info: [],
|
|
64
|
+
debug: ["info"],
|
|
65
|
+
trace: ["debug", "info"]
|
|
66
|
+
};
|
|
67
|
+
var callBackendMethod = (backend, level, message, payload) => {
|
|
68
|
+
if (typeof backend[level] !== "function") {
|
|
69
|
+
throw new ChroniclerError("BACKEND_METHOD", `Backend does not support log level: ${level}`);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
backend[level](message, payload);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(
|
|
75
|
+
"[chronicler] Backend error during log emission:",
|
|
76
|
+
err instanceof Error ? err.message : "Unknown error"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var createConsoleBackend = () => {
|
|
81
|
+
const backend = {};
|
|
82
|
+
for (const level of DEFAULT_REQUIRED_LEVELS) {
|
|
83
|
+
const method = CONSOLE_LEVEL_MAP[level];
|
|
84
|
+
backend[level] = (message, payload) => console[method](message, payload);
|
|
85
|
+
}
|
|
86
|
+
return backend;
|
|
87
|
+
};
|
|
88
|
+
var createBackend = (partial) => {
|
|
89
|
+
const backend = {};
|
|
90
|
+
for (const level of DEFAULT_REQUIRED_LEVELS) {
|
|
91
|
+
if (typeof partial[level] === "function") {
|
|
92
|
+
backend[level] = partial[level];
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const fallback = LEVEL_FALLBACK_CHAINS[level].find((fb) => typeof partial[fb] === "function");
|
|
96
|
+
if (fallback) {
|
|
97
|
+
backend[level] = partial[fallback];
|
|
98
|
+
} else {
|
|
99
|
+
const method = CONSOLE_LEVEL_MAP[level];
|
|
100
|
+
backend[level] = (message, payload) => console[method](message, payload);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return backend;
|
|
104
|
+
};
|
|
105
|
+
var createRouterBackend = (routes) => {
|
|
106
|
+
if (routes.length === 0) {
|
|
107
|
+
throw new Error("createRouterBackend requires at least one route.");
|
|
108
|
+
}
|
|
109
|
+
const backend = {};
|
|
110
|
+
for (const level of DEFAULT_REQUIRED_LEVELS) {
|
|
111
|
+
backend[level] = (message, payload) => {
|
|
112
|
+
for (const route of routes) {
|
|
113
|
+
if (!route.filter || route.filter(level, payload)) {
|
|
114
|
+
callBackendMethod(route.backend, level, message, payload);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return backend;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/core/reserved.ts
|
|
123
|
+
var RESERVED_TOP_LEVEL_FIELDS = [
|
|
124
|
+
"eventKey",
|
|
125
|
+
"level",
|
|
126
|
+
"message",
|
|
127
|
+
"correlationId",
|
|
128
|
+
"forkId",
|
|
129
|
+
"timestamp",
|
|
130
|
+
"fields",
|
|
131
|
+
"_validation"
|
|
132
|
+
];
|
|
133
|
+
var TOP_LEVEL_SET = new Set(RESERVED_TOP_LEVEL_FIELDS);
|
|
134
|
+
var isReservedTopLevelField = (key) => TOP_LEVEL_SET.has(key);
|
|
135
|
+
var assertNoReservedKeys = (record) => {
|
|
136
|
+
const invalid = [];
|
|
137
|
+
for (const key of Object.keys(record)) {
|
|
138
|
+
if (isReservedTopLevelField(key)) {
|
|
139
|
+
invalid.push(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return invalid;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/core/context.ts
|
|
146
|
+
var isSimpleValue = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
147
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set([
|
|
148
|
+
"__proto__",
|
|
149
|
+
"constructor",
|
|
150
|
+
"prototype",
|
|
151
|
+
"toString",
|
|
152
|
+
"valueOf",
|
|
153
|
+
"hasOwnProperty",
|
|
154
|
+
"isPrototypeOf",
|
|
155
|
+
"propertyIsEnumerable",
|
|
156
|
+
"toLocaleString",
|
|
157
|
+
"__defineGetter__",
|
|
158
|
+
"__defineSetter__",
|
|
159
|
+
"__lookupGetter__",
|
|
160
|
+
"__lookupSetter__"
|
|
161
|
+
]);
|
|
162
|
+
var sanitizeContextInput = (context, existingContext = {}, maxNewKeys = Infinity) => {
|
|
163
|
+
const sanitized = {};
|
|
164
|
+
const reserved = [];
|
|
165
|
+
const collisionDetails = [];
|
|
166
|
+
const dropped = [];
|
|
167
|
+
let acceptedCount = 0;
|
|
168
|
+
for (const [key, rawValue] of Object.entries(context)) {
|
|
169
|
+
if (isReservedTopLevelField(key)) {
|
|
170
|
+
reserved.push(key);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
174
|
+
reserved.push(key);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (!isSimpleValue(rawValue)) continue;
|
|
178
|
+
if (Object.hasOwn(existingContext, key) || Object.hasOwn(sanitized, key)) {
|
|
179
|
+
const existingValue = Object.hasOwn(sanitized, key) ? sanitized[key] : existingContext[key];
|
|
180
|
+
collisionDetails.push({ key, existingValue, attemptedValue: rawValue });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (acceptedCount >= maxNewKeys) {
|
|
184
|
+
dropped.push(key);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
sanitized[key] = rawValue;
|
|
188
|
+
acceptedCount++;
|
|
189
|
+
}
|
|
190
|
+
return { context: sanitized, validation: { reserved, collisionDetails, dropped } };
|
|
191
|
+
};
|
|
192
|
+
var ContextStore = class {
|
|
193
|
+
context = {};
|
|
194
|
+
maxKeys;
|
|
195
|
+
constructor(initial = {}, maxKeys = Infinity) {
|
|
196
|
+
this.maxKeys = maxKeys;
|
|
197
|
+
const { context } = sanitizeContextInput(initial, {}, maxKeys);
|
|
198
|
+
this.context = context;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Merge new context into the store.
|
|
202
|
+
*
|
|
203
|
+
* @param raw - Key-value pairs to add
|
|
204
|
+
* @returns Validation result with any collisions or reserved field attempts
|
|
205
|
+
*/
|
|
206
|
+
add(raw) {
|
|
207
|
+
const remaining = Math.max(0, this.maxKeys - Object.keys(this.context).length);
|
|
208
|
+
const { context, validation } = sanitizeContextInput(raw, this.context, remaining);
|
|
209
|
+
Object.assign(this.context, context);
|
|
210
|
+
return validation;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Return a shallow copy of the current context.
|
|
214
|
+
*
|
|
215
|
+
* @returns A new object containing all accumulated context key-value pairs
|
|
216
|
+
*/
|
|
217
|
+
snapshot() {
|
|
218
|
+
return { ...this.context };
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/core/fields.ts
|
|
223
|
+
function makeOptional(type, doc) {
|
|
224
|
+
return {
|
|
225
|
+
_type: type,
|
|
226
|
+
_required: false,
|
|
227
|
+
_doc: doc,
|
|
228
|
+
doc: (description) => makeOptional(type, description)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function makeRequired(type, doc) {
|
|
232
|
+
return {
|
|
233
|
+
_type: type,
|
|
234
|
+
_required: true,
|
|
235
|
+
_doc: doc,
|
|
236
|
+
optional: () => makeOptional(type, doc),
|
|
237
|
+
doc: (description) => makeRequired(type, description)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
var field = {
|
|
241
|
+
string: () => makeRequired("string", void 0),
|
|
242
|
+
number: () => makeRequired("number", void 0),
|
|
243
|
+
boolean: () => makeRequired("boolean", void 0),
|
|
244
|
+
error: () => makeRequired("error", void 0)
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/core/events.ts
|
|
248
|
+
var correlationAutoFields = {
|
|
249
|
+
complete: {
|
|
250
|
+
duration: field.number().optional().doc("Duration of the correlation in milliseconds")
|
|
251
|
+
},
|
|
252
|
+
fail: {
|
|
253
|
+
duration: field.number().optional().doc("Duration of the correlation in milliseconds"),
|
|
254
|
+
error: field.error().optional().doc("Error that caused the failure")
|
|
255
|
+
}};
|
|
256
|
+
var MAX_EVENT_KEY_LENGTH = 256;
|
|
257
|
+
var EVENT_KEY_RE = /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)*$/;
|
|
258
|
+
var validateEventKey = (key, label) => {
|
|
259
|
+
if (key.length > MAX_EVENT_KEY_LENGTH) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`${label} key "${key.slice(0, 50)}..." exceeds maximum length of ${MAX_EVENT_KEY_LENGTH} characters.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (!EVENT_KEY_RE.test(key)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Invalid ${label} key "${key}". Keys must be dotted camelCase identifiers (e.g. "user.created", "http.request.started").`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var defineEvent = (event) => {
|
|
271
|
+
validateEventKey(event.key, "Event");
|
|
272
|
+
return event;
|
|
273
|
+
};
|
|
274
|
+
var prefixEventKeys = (groupKey, events) => {
|
|
275
|
+
if (!events) return events;
|
|
276
|
+
const result = {};
|
|
277
|
+
for (const [name, event] of Object.entries(events)) {
|
|
278
|
+
if (event.key.startsWith(`${groupKey}.`)) {
|
|
279
|
+
result[name] = event;
|
|
280
|
+
} else {
|
|
281
|
+
result[name] = { ...event, key: `${groupKey}.${name}` };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
};
|
|
286
|
+
var defineEventGroup = (group) => {
|
|
287
|
+
validateEventKey(group.key, "Group");
|
|
288
|
+
const prefixed = prefixEventKeys(group.key, group.events);
|
|
289
|
+
if (prefixed !== group.events) {
|
|
290
|
+
return { ...group, events: prefixed };
|
|
291
|
+
}
|
|
292
|
+
return group;
|
|
293
|
+
};
|
|
294
|
+
var buildAutoEvents = (groupKey) => ({
|
|
295
|
+
start: {
|
|
296
|
+
key: `${groupKey}.start`,
|
|
297
|
+
level: "info",
|
|
298
|
+
message: `${groupKey} started`,
|
|
299
|
+
doc: "Auto-generated correlation start event"
|
|
300
|
+
},
|
|
301
|
+
complete: {
|
|
302
|
+
key: `${groupKey}.complete`,
|
|
303
|
+
level: "info",
|
|
304
|
+
message: `${groupKey} completed`,
|
|
305
|
+
doc: "Auto-generated correlation completion event",
|
|
306
|
+
fields: correlationAutoFields.complete
|
|
307
|
+
},
|
|
308
|
+
fail: {
|
|
309
|
+
key: `${groupKey}.fail`,
|
|
310
|
+
level: "error",
|
|
311
|
+
message: `${groupKey} failed`,
|
|
312
|
+
doc: "Auto-generated correlation failure event",
|
|
313
|
+
fields: correlationAutoFields.fail
|
|
314
|
+
},
|
|
315
|
+
timeout: {
|
|
316
|
+
key: `${groupKey}.timeout`,
|
|
317
|
+
level: "warn",
|
|
318
|
+
message: `${groupKey} timed out`,
|
|
319
|
+
doc: "Auto-generated correlation timeout event"
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
var defineCorrelationGroup = (group) => {
|
|
323
|
+
validateEventKey(group.key, "Group");
|
|
324
|
+
const autoEvents = buildAutoEvents(group.key);
|
|
325
|
+
const prefixed = prefixEventKeys(group.key, group.events) ?? {};
|
|
326
|
+
const autoKeys = Object.keys(autoEvents);
|
|
327
|
+
const conflicts = autoKeys.filter((k) => Object.hasOwn(prefixed, k));
|
|
328
|
+
if (conflicts.length > 0) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Correlation group "${group.key}" defines events that collide with auto-generated lifecycle events: ${conflicts.join(", ")}. Rename these events to avoid the conflict.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
...group,
|
|
335
|
+
timeout: group.timeout ?? DEFAULT_CORRELATION_TIMEOUT_MS,
|
|
336
|
+
events: {
|
|
337
|
+
...prefixed,
|
|
338
|
+
...autoEvents
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/core/validation.ts
|
|
344
|
+
var ANSI_ESCAPE_RE = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
345
|
+
var NEWLINE_RE = /[\r\n]/g;
|
|
346
|
+
var sanitizeString = (value) => value.replace(ANSI_ESCAPE_RE, "").replace(NEWLINE_RE, "\\n");
|
|
347
|
+
var sanitizeLogFields = (fields) => {
|
|
348
|
+
const result = {};
|
|
349
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
350
|
+
result[key] = typeof value === "string" ? sanitizeString(value) : value;
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
};
|
|
354
|
+
var checkFieldType = (value, type) => {
|
|
355
|
+
switch (type) {
|
|
356
|
+
case "error":
|
|
357
|
+
return value instanceof Error || typeof value === "string" ? "ok" : "type_error";
|
|
358
|
+
case "string":
|
|
359
|
+
return typeof value === "string" ? "ok" : "type_error";
|
|
360
|
+
case "number":
|
|
361
|
+
if (typeof value !== "number") return "type_error";
|
|
362
|
+
return Number.isFinite(value) ? "ok" : "invalid_value";
|
|
363
|
+
case "boolean":
|
|
364
|
+
return typeof value === "boolean" ? "ok" : "type_error";
|
|
365
|
+
default:
|
|
366
|
+
return "type_error";
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var validateFields = (event, payload) => {
|
|
370
|
+
const providedFields = payload ?? {};
|
|
371
|
+
const normalizedFields = {};
|
|
372
|
+
const missingFields = [];
|
|
373
|
+
const typeErrors = [];
|
|
374
|
+
const invalidValues = [];
|
|
375
|
+
const unknownFields = [];
|
|
376
|
+
const fieldBuilders = event.fields ?? {};
|
|
377
|
+
const definedFieldNames = new Set(Object.keys(fieldBuilders));
|
|
378
|
+
for (const [name, builder] of Object.entries(fieldBuilders)) {
|
|
379
|
+
const value = providedFields[name];
|
|
380
|
+
const fieldType = builder._type;
|
|
381
|
+
const isRequired = builder._required;
|
|
382
|
+
if (value === void 0 || value === null) {
|
|
383
|
+
if (isRequired) {
|
|
384
|
+
missingFields.push(name);
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const typeCheck = checkFieldType(value, fieldType);
|
|
389
|
+
if (typeCheck === "type_error") {
|
|
390
|
+
typeErrors.push(name);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (typeCheck === "invalid_value") {
|
|
394
|
+
invalidValues.push(name);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (fieldType === "error") {
|
|
398
|
+
try {
|
|
399
|
+
normalizedFields[name] = value instanceof Error ? value.stack ?? value.message : typeof value === "string" ? value : "[unknown error]";
|
|
400
|
+
} catch {
|
|
401
|
+
normalizedFields[name] = "[unserializable error]";
|
|
402
|
+
}
|
|
403
|
+
} else if (fieldType === "string" && typeof value === "string") {
|
|
404
|
+
normalizedFields[name] = sanitizeString(value);
|
|
405
|
+
} else {
|
|
406
|
+
normalizedFields[name] = value;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const [name, value] of Object.entries(providedFields)) {
|
|
410
|
+
if (!definedFieldNames.has(name) && value !== void 0 && value !== null) {
|
|
411
|
+
unknownFields.push(name);
|
|
412
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (typeof value === "string") {
|
|
416
|
+
normalizedFields[name] = sanitizeString(value);
|
|
417
|
+
} else {
|
|
418
|
+
normalizedFields[name] = value;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { missingFields, typeErrors, invalidValues, unknownFields, normalizedFields };
|
|
423
|
+
};
|
|
424
|
+
var buildValidationMetadata = (fieldValidation) => {
|
|
425
|
+
const entries = Object.entries({
|
|
426
|
+
missingFields: fieldValidation.missingFields,
|
|
427
|
+
typeErrors: fieldValidation.typeErrors,
|
|
428
|
+
invalidValues: fieldValidation.invalidValues,
|
|
429
|
+
unknownFields: fieldValidation.unknownFields
|
|
430
|
+
}).filter(([, v]) => Array.isArray(v) && v.length > 0);
|
|
431
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/core/chronicle.ts
|
|
435
|
+
var buildPayload = (args) => {
|
|
436
|
+
const fieldValidation = validateFields(args.eventDef, args.fields);
|
|
437
|
+
if (args.strict) {
|
|
438
|
+
const issues = [];
|
|
439
|
+
if (fieldValidation.missingFields.length > 0) {
|
|
440
|
+
issues.push(`missing required fields: ${fieldValidation.missingFields.join(", ")}`);
|
|
441
|
+
}
|
|
442
|
+
if (fieldValidation.typeErrors.length > 0) {
|
|
443
|
+
issues.push(`type errors on fields: ${fieldValidation.typeErrors.join(", ")}`);
|
|
444
|
+
}
|
|
445
|
+
if (fieldValidation.invalidValues.length > 0) {
|
|
446
|
+
issues.push(`invalid values on fields: ${fieldValidation.invalidValues.join(", ")}`);
|
|
447
|
+
}
|
|
448
|
+
if (issues.length > 0) {
|
|
449
|
+
throw new ChroniclerError(
|
|
450
|
+
"FIELD_VALIDATION",
|
|
451
|
+
`Event "${args.eventDef.key}" failed validation: ${issues.join("; ")}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const validationMetadata = buildValidationMetadata(fieldValidation);
|
|
456
|
+
return {
|
|
457
|
+
eventKey: args.eventDef.key,
|
|
458
|
+
fields: fieldValidation.normalizedFields,
|
|
459
|
+
correlationId: args.currentCorrelationId(),
|
|
460
|
+
forkId: args.forkId,
|
|
461
|
+
metadata: args.contextStore.snapshot(),
|
|
462
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
463
|
+
...validationMetadata ? { _validation: validationMetadata } : {}
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
var forkDepthFromId = (forkId) => forkId === ROOT_FORK_ID ? 0 : forkId.split(FORK_ID_SEPARATOR).length;
|
|
467
|
+
var nextForkId = (parentForkId, counter, maxDepth) => {
|
|
468
|
+
const childForkId = parentForkId === ROOT_FORK_ID ? String(counter) : `${parentForkId}${FORK_ID_SEPARATOR}${counter}`;
|
|
469
|
+
const depth = forkDepthFromId(childForkId);
|
|
470
|
+
if (depth > maxDepth) {
|
|
471
|
+
throw new ChroniclerError(
|
|
472
|
+
"FORK_DEPTH_EXCEEDED",
|
|
473
|
+
`Fork depth ${depth} exceeds maximum allowed depth of ${maxDepth}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return childForkId;
|
|
477
|
+
};
|
|
478
|
+
var isAlreadyNormalized = (group) => typeof group.timeout === "number" && group.events !== void 0 && "start" in group.events;
|
|
479
|
+
var resolveCorrelationGroup = (group) => isAlreadyNormalized(group) ? group : defineCorrelationGroup(group);
|
|
480
|
+
var createChronicleInstance = (args) => {
|
|
481
|
+
const {
|
|
482
|
+
config,
|
|
483
|
+
contextStore,
|
|
484
|
+
currentCorrelationId,
|
|
485
|
+
correlationIdGenerator,
|
|
486
|
+
forkId,
|
|
487
|
+
hooks = {},
|
|
488
|
+
activeCorrelations = { count: 0 }
|
|
489
|
+
} = args;
|
|
490
|
+
let forkCounter = 0;
|
|
491
|
+
return {
|
|
492
|
+
event(eventDef, fields) {
|
|
493
|
+
if (LOG_LEVELS[eventDef.level] > config.minLevel) return;
|
|
494
|
+
const payload = buildPayload({
|
|
495
|
+
contextStore,
|
|
496
|
+
eventDef,
|
|
497
|
+
// Deliberate type erasure: EventFields<E> → Record<string, unknown>
|
|
498
|
+
fields,
|
|
499
|
+
currentCorrelationId,
|
|
500
|
+
forkId,
|
|
501
|
+
strict: config.strict
|
|
502
|
+
});
|
|
503
|
+
callBackendMethod(config.backend, eventDef.level, eventDef.message, payload);
|
|
504
|
+
hooks.onActivity?.();
|
|
505
|
+
},
|
|
506
|
+
log(level, message, fields = {}) {
|
|
507
|
+
if (LOG_LEVELS[level] > config.minLevel) return;
|
|
508
|
+
const payload = {
|
|
509
|
+
eventKey: "",
|
|
510
|
+
fields: sanitizeLogFields(fields),
|
|
511
|
+
correlationId: currentCorrelationId(),
|
|
512
|
+
forkId,
|
|
513
|
+
metadata: contextStore.snapshot(),
|
|
514
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
515
|
+
};
|
|
516
|
+
callBackendMethod(config.backend, level, message, payload);
|
|
517
|
+
hooks.onActivity?.();
|
|
518
|
+
},
|
|
519
|
+
addContext(context) {
|
|
520
|
+
return contextStore.add(context);
|
|
521
|
+
},
|
|
522
|
+
fork(extraContext = {}) {
|
|
523
|
+
forkCounter++;
|
|
524
|
+
const childForkId = nextForkId(forkId, forkCounter, config.limits.maxForkDepth);
|
|
525
|
+
const forkStore = new ContextStore(contextStore.snapshot(), config.limits.maxContextKeys);
|
|
526
|
+
const forkChronicle = createChronicleInstance({
|
|
527
|
+
config,
|
|
528
|
+
contextStore: forkStore,
|
|
529
|
+
currentCorrelationId,
|
|
530
|
+
correlationIdGenerator,
|
|
531
|
+
forkId: childForkId,
|
|
532
|
+
hooks,
|
|
533
|
+
activeCorrelations
|
|
534
|
+
});
|
|
535
|
+
if (Object.keys(extraContext).length > 0) {
|
|
536
|
+
forkChronicle.addContext(extraContext);
|
|
537
|
+
}
|
|
538
|
+
return forkChronicle;
|
|
539
|
+
},
|
|
540
|
+
startCorrelation(group, metadata = {}) {
|
|
541
|
+
if (activeCorrelations.count >= config.limits.maxActiveCorrelations) {
|
|
542
|
+
throw new ChroniclerError(
|
|
543
|
+
"CORRELATION_LIMIT_EXCEEDED",
|
|
544
|
+
`Active correlation limit of ${config.limits.maxActiveCorrelations} exceeded`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const definedGroup = resolveCorrelationGroup(group);
|
|
548
|
+
activeCorrelations.count++;
|
|
549
|
+
const correlationStore = new ContextStore(
|
|
550
|
+
contextStore.snapshot(),
|
|
551
|
+
config.limits.maxContextKeys
|
|
552
|
+
);
|
|
553
|
+
if (Object.keys(metadata).length > 0) {
|
|
554
|
+
correlationStore.add(metadata);
|
|
555
|
+
}
|
|
556
|
+
const correlationId = correlationIdGenerator();
|
|
557
|
+
return new CorrelationChronicleImpl({
|
|
558
|
+
config,
|
|
559
|
+
group: definedGroup,
|
|
560
|
+
contextStore: correlationStore,
|
|
561
|
+
currentCorrelationId: () => correlationId,
|
|
562
|
+
correlationIdGenerator,
|
|
563
|
+
forkId,
|
|
564
|
+
activeCorrelations
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
var CorrelationTimer = class {
|
|
570
|
+
constructor(timeout, onTimeout) {
|
|
571
|
+
this.timeout = timeout;
|
|
572
|
+
this.onTimeout = onTimeout;
|
|
573
|
+
}
|
|
574
|
+
timeoutId;
|
|
575
|
+
start() {
|
|
576
|
+
this.clear();
|
|
577
|
+
if (this.timeout > 0) {
|
|
578
|
+
this.timeoutId = setTimeout(this.onTimeout, this.timeout);
|
|
579
|
+
this.timeoutId.unref();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/** Reset the timer (keep-alive on activity). */
|
|
583
|
+
touch() {
|
|
584
|
+
this.start();
|
|
585
|
+
}
|
|
586
|
+
clear() {
|
|
587
|
+
if (this.timeoutId !== void 0) {
|
|
588
|
+
clearTimeout(this.timeoutId);
|
|
589
|
+
this.timeoutId = void 0;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
var CorrelationChronicleImpl = class {
|
|
594
|
+
config;
|
|
595
|
+
contextStore;
|
|
596
|
+
currentCorrelationId;
|
|
597
|
+
correlationIdGenerator;
|
|
598
|
+
forkId;
|
|
599
|
+
activeCorrelations;
|
|
600
|
+
timer;
|
|
601
|
+
completed = false;
|
|
602
|
+
startedAt = Date.now();
|
|
603
|
+
autoEvents;
|
|
604
|
+
forkCounter = 0;
|
|
605
|
+
constructor(args) {
|
|
606
|
+
this.config = args.config;
|
|
607
|
+
this.contextStore = args.contextStore;
|
|
608
|
+
this.currentCorrelationId = args.currentCorrelationId;
|
|
609
|
+
this.correlationIdGenerator = args.correlationIdGenerator;
|
|
610
|
+
this.forkId = args.forkId;
|
|
611
|
+
this.activeCorrelations = args.activeCorrelations ?? { count: 0 };
|
|
612
|
+
this.timer = new CorrelationTimer(args.group.timeout, () => this.timeout());
|
|
613
|
+
this.autoEvents = args.group.events;
|
|
614
|
+
this.timer.start();
|
|
615
|
+
this.emitAutoEvent(this.autoEvents.start, {});
|
|
616
|
+
}
|
|
617
|
+
event(eventDef, fields) {
|
|
618
|
+
if (this.completed) return;
|
|
619
|
+
if (LOG_LEVELS[eventDef.level] > this.config.minLevel) return;
|
|
620
|
+
const payload = buildPayload({
|
|
621
|
+
contextStore: this.contextStore,
|
|
622
|
+
eventDef,
|
|
623
|
+
// Deliberate type erasure: EventFields<E> → Record<string, unknown>
|
|
624
|
+
fields,
|
|
625
|
+
currentCorrelationId: this.currentCorrelationId,
|
|
626
|
+
forkId: this.forkId,
|
|
627
|
+
strict: this.config.strict
|
|
628
|
+
});
|
|
629
|
+
callBackendMethod(this.config.backend, eventDef.level, eventDef.message, payload);
|
|
630
|
+
this.timer.touch();
|
|
631
|
+
}
|
|
632
|
+
log(level, message, fields = {}) {
|
|
633
|
+
if (this.completed) return;
|
|
634
|
+
if (LOG_LEVELS[level] > this.config.minLevel) return;
|
|
635
|
+
const payload = {
|
|
636
|
+
eventKey: "",
|
|
637
|
+
fields: sanitizeLogFields(fields),
|
|
638
|
+
correlationId: this.currentCorrelationId(),
|
|
639
|
+
forkId: this.forkId,
|
|
640
|
+
metadata: this.contextStore.snapshot(),
|
|
641
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
642
|
+
};
|
|
643
|
+
callBackendMethod(this.config.backend, level, message, payload);
|
|
644
|
+
this.timer.touch();
|
|
645
|
+
}
|
|
646
|
+
addContext(context) {
|
|
647
|
+
return this.contextStore.add(context);
|
|
648
|
+
}
|
|
649
|
+
fork(extraContext = {}) {
|
|
650
|
+
this.forkCounter++;
|
|
651
|
+
const childForkId = nextForkId(this.forkId, this.forkCounter, this.config.limits.maxForkDepth);
|
|
652
|
+
const forkStore = new ContextStore(
|
|
653
|
+
this.contextStore.snapshot(),
|
|
654
|
+
this.config.limits.maxContextKeys
|
|
655
|
+
);
|
|
656
|
+
const forkChronicle = createChronicleInstance({
|
|
657
|
+
config: this.config,
|
|
658
|
+
contextStore: forkStore,
|
|
659
|
+
currentCorrelationId: this.currentCorrelationId,
|
|
660
|
+
correlationIdGenerator: this.correlationIdGenerator,
|
|
661
|
+
forkId: childForkId,
|
|
662
|
+
hooks: { onActivity: () => this.timer.touch() },
|
|
663
|
+
activeCorrelations: this.activeCorrelations
|
|
664
|
+
});
|
|
665
|
+
if (Object.keys(extraContext).length > 0) {
|
|
666
|
+
forkChronicle.addContext(extraContext);
|
|
667
|
+
}
|
|
668
|
+
return forkChronicle;
|
|
669
|
+
}
|
|
670
|
+
complete(fields = {}) {
|
|
671
|
+
if (!this.finalize()) return;
|
|
672
|
+
this.emitAutoEvent(this.autoEvents.complete, {
|
|
673
|
+
duration: Date.now() - this.startedAt,
|
|
674
|
+
...fields
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
fail(error, fields = {}) {
|
|
678
|
+
if (!this.finalize()) return;
|
|
679
|
+
this.emitAutoEvent(this.autoEvents.fail, {
|
|
680
|
+
duration: Date.now() - this.startedAt,
|
|
681
|
+
error,
|
|
682
|
+
...fields
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
timeout() {
|
|
686
|
+
if (!this.finalize()) return;
|
|
687
|
+
this.emitAutoEvent(this.autoEvents.timeout, {});
|
|
688
|
+
}
|
|
689
|
+
/** Mark correlation as done: decrement counter, clear timer. Returns false if already completed. */
|
|
690
|
+
finalize() {
|
|
691
|
+
if (this.completed) return false;
|
|
692
|
+
this.activeCorrelations.count--;
|
|
693
|
+
this.completed = true;
|
|
694
|
+
this.timer.clear();
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
emitAutoEvent(eventDef, fields) {
|
|
698
|
+
if (LOG_LEVELS[eventDef.level] > this.config.minLevel) return;
|
|
699
|
+
const payload = buildPayload({
|
|
700
|
+
contextStore: this.contextStore,
|
|
701
|
+
eventDef,
|
|
702
|
+
fields,
|
|
703
|
+
currentCorrelationId: this.currentCorrelationId,
|
|
704
|
+
forkId: this.forkId,
|
|
705
|
+
strict: this.config.strict
|
|
706
|
+
});
|
|
707
|
+
callBackendMethod(this.config.backend, eventDef.level, eventDef.message, payload);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
var resolveChroniclerConfig = (config) => {
|
|
711
|
+
const resolvedBackend = config.backend ?? createConsoleBackend();
|
|
712
|
+
const missingLevels = DEFAULT_REQUIRED_LEVELS.filter(
|
|
713
|
+
(level) => typeof resolvedBackend[level] !== "function"
|
|
714
|
+
);
|
|
715
|
+
if (missingLevels.length > 0) {
|
|
716
|
+
throw new ChroniclerError(
|
|
717
|
+
"UNSUPPORTED_LOG_LEVEL",
|
|
718
|
+
`Log backend is missing level(s): ${missingLevels.join(", ")}. A valid backend must implement all 9 levels: ${DEFAULT_REQUIRED_LEVELS.join(", ")}. Use createBackend() for automatic fallback handling.`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const reservedMetadata = assertNoReservedKeys(config.metadata);
|
|
722
|
+
if (reservedMetadata.length > 0) {
|
|
723
|
+
throw new ChroniclerError(
|
|
724
|
+
"RESERVED_FIELD",
|
|
725
|
+
`Reserved fields cannot be used in metadata: ${reservedMetadata.join(", ")}`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const resolvedLimits = {
|
|
729
|
+
maxContextKeys: config.limits?.maxContextKeys ?? DEFAULT_MAX_CONTEXT_KEYS,
|
|
730
|
+
maxForkDepth: config.limits?.maxForkDepth ?? DEFAULT_MAX_FORK_DEPTH,
|
|
731
|
+
maxActiveCorrelations: config.limits?.maxActiveCorrelations ?? DEFAULT_MAX_ACTIVE_CORRELATIONS
|
|
732
|
+
};
|
|
733
|
+
return {
|
|
734
|
+
resolved: {
|
|
735
|
+
backend: resolvedBackend,
|
|
736
|
+
limits: resolvedLimits,
|
|
737
|
+
minLevel: LOG_LEVELS[config.minLevel ?? "trace"],
|
|
738
|
+
strict: config.strict
|
|
739
|
+
},
|
|
740
|
+
correlationIdGenerator: config.correlationIdGenerator ?? (() => crypto.randomUUID())
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
var createChronicle = (config) => {
|
|
744
|
+
const { resolved, correlationIdGenerator } = resolveChroniclerConfig(config);
|
|
745
|
+
const baseContextStore = new ContextStore(config.metadata, resolved.limits.maxContextKeys);
|
|
746
|
+
return createChronicleInstance({
|
|
747
|
+
config: resolved,
|
|
748
|
+
contextStore: baseContextStore,
|
|
749
|
+
currentCorrelationId: () => "",
|
|
750
|
+
correlationIdGenerator,
|
|
751
|
+
forkId: ROOT_FORK_ID,
|
|
752
|
+
/** Shared mutable counter tracking uncompleted correlations for limit enforcement. */
|
|
753
|
+
activeCorrelations: { count: 0 }
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
export { ChroniclerError, createBackend, createChronicle, createConsoleBackend, createRouterBackend, defineCorrelationGroup, defineEvent, defineEventGroup, field };
|
|
758
|
+
//# sourceMappingURL=index.js.map
|
|
759
|
+
//# sourceMappingURL=index.js.map
|