@logbrew/sdk 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/index.cjs ADDED
@@ -0,0 +1,1099 @@
1
+ const ISSUE_LEVELS = new Set(["info", "warning", "error", "critical"]);
2
+ const LOG_LEVELS = new Set(["debug", "info", "warning", "error"]);
3
+ const SPAN_STATUSES = new Set(["ok", "error"]);
4
+ const ACTION_STATUSES = new Set(["queued", "running", "success", "failure"]);
5
+ const CONSOLE_METHODS = new Set(["debug", "info", "log", "warn", "error"]);
6
+ const DEFAULT_CONSOLE_LEVELS = ["debug", "info", "log", "warn", "error"];
7
+ const PINO_HOST_FIELD = ["host", "name"].join("");
8
+ const PINO_RESERVED_FIELDS = new Set(["level", "time", "timestamp", "msg", "message", "err", "error", "pid", PINO_HOST_FIELD, "v"]);
9
+ const WINSTON_RESERVED_FIELDS = new Set(["level", "message", "timestamp", "time", "err", "error", "stack"]);
10
+ const TRACEPARENT_PATTERN = /^([0-9a-fA-F]{2})-([0-9a-fA-F]{32})-([0-9a-fA-F]{16})-([0-9a-fA-F]{2})$/u;
11
+ const ZERO_TRACE_ID = "00000000000000000000000000000000";
12
+ const ZERO_SPAN_ID = "0000000000000000";
13
+
14
+ class SdkError extends Error {
15
+ constructor(code, message) {
16
+ super(message);
17
+ this.name = "SdkError";
18
+ this.code = code;
19
+ }
20
+ }
21
+
22
+ class TransportError extends Error {
23
+ constructor(code, message, retryable = false) {
24
+ super(message);
25
+ this.name = "TransportError";
26
+ this.code = code;
27
+ this.retryable = retryable;
28
+ }
29
+
30
+ static network(message) {
31
+ return new TransportError("network_failure", message, true);
32
+ }
33
+ }
34
+
35
+ class RecordingTransport {
36
+ constructor(scriptedResponses = [{ statusCode: 202 }]) {
37
+ this.scriptedResponses = [...scriptedResponses];
38
+ this.sentBodies = [];
39
+ }
40
+
41
+ static alwaysAccept() {
42
+ return new RecordingTransport([{ statusCode: 202 }]);
43
+ }
44
+
45
+ lastBody() {
46
+ return this.sentBodies.at(-1) ?? null;
47
+ }
48
+
49
+ async send(apiKey, body) {
50
+ requireNonEmpty("apiKey", apiKey);
51
+ this.sentBodies.push(body);
52
+
53
+ const next = this.scriptedResponses.length > 0
54
+ ? this.scriptedResponses.shift()
55
+ : { statusCode: 202 };
56
+
57
+ if (next instanceof Error) {
58
+ throw next;
59
+ }
60
+
61
+ return { statusCode: next.statusCode, attempts: 1 };
62
+ }
63
+ }
64
+
65
+ class LogBrewClient {
66
+ static create({ apiKey, sdkName, sdkVersion, maxRetries = 2 }) {
67
+ requireNonEmpty("apiKey", apiKey);
68
+ requireNonEmpty("sdkName", sdkName);
69
+ requireNonEmpty("sdkVersion", sdkVersion);
70
+
71
+ return new LogBrewClient({
72
+ apiKey,
73
+ sdk: {
74
+ name: sdkName,
75
+ language: "javascript",
76
+ version: sdkVersion
77
+ },
78
+ maxRetries
79
+ });
80
+ }
81
+
82
+ constructor({ apiKey, sdk, maxRetries }) {
83
+ this.apiKey = apiKey;
84
+ this.sdk = sdk;
85
+ this.maxRetries = maxRetries;
86
+ this.events = [];
87
+ this.closed = false;
88
+ }
89
+
90
+ pendingEvents() {
91
+ return this.events.length;
92
+ }
93
+
94
+ previewJson() {
95
+ return JSON.stringify({ sdk: this.sdk, events: this.events }, null, 2);
96
+ }
97
+
98
+ release(id, timestamp, attributes) {
99
+ this.#pushEvent("release", id, timestamp, validateRelease(attributes));
100
+ }
101
+
102
+ environment(id, timestamp, attributes) {
103
+ this.#pushEvent("environment", id, timestamp, validateEnvironment(attributes));
104
+ }
105
+
106
+ issue(id, timestamp, attributes) {
107
+ this.#pushEvent("issue", id, timestamp, validateIssue(attributes));
108
+ }
109
+
110
+ log(id, timestamp, attributes) {
111
+ this.#pushEvent("log", id, timestamp, validateLog(attributes));
112
+ }
113
+
114
+ span(id, timestamp, attributes) {
115
+ this.#pushEvent("span", id, timestamp, validateSpan(attributes));
116
+ }
117
+
118
+ action(id, timestamp, attributes) {
119
+ this.#pushEvent("action", id, timestamp, validateAction(attributes));
120
+ }
121
+
122
+ async flush(transport) {
123
+ if (this.closed) {
124
+ throw new SdkError("shutdown_error", "client is already shut down");
125
+ }
126
+ return this.#flushInternal(transport);
127
+ }
128
+
129
+ async shutdown(transport) {
130
+ if (this.closed) {
131
+ throw new SdkError("shutdown_error", "client is already shut down");
132
+ }
133
+ const response = await this.#flushInternal(transport);
134
+ this.closed = true;
135
+ return response;
136
+ }
137
+
138
+ #pushEvent(eventType, id, timestamp, attributes) {
139
+ if (this.closed) {
140
+ throw new SdkError("shutdown_error", "client is already shut down");
141
+ }
142
+ requireNonEmpty("event id", id);
143
+ requireTimestamp(timestamp);
144
+ this.events.push({ type: eventType, id, timestamp, attributes });
145
+ }
146
+
147
+ async #flushInternal(transport) {
148
+ if (this.events.length === 0) {
149
+ return { statusCode: 204, attempts: 0 };
150
+ }
151
+
152
+ const body = this.previewJson();
153
+ const maxAttempts = this.maxRetries + 1;
154
+ let attempts = 0;
155
+
156
+ while (attempts < maxAttempts) {
157
+ attempts += 1;
158
+ try {
159
+ const response = await transport.send(this.apiKey, body);
160
+ if (response.statusCode === 401) {
161
+ throw new SdkError("unauthenticated", "transport rejected the API key");
162
+ }
163
+ if (response.statusCode >= 200 && response.statusCode < 300) {
164
+ this.events = [];
165
+ return { statusCode: response.statusCode, attempts };
166
+ }
167
+ if (response.statusCode >= 500 && attempts < maxAttempts) {
168
+ continue;
169
+ }
170
+ throw new SdkError("transport_error", `unexpected transport status ${response.statusCode}`);
171
+ } catch (error) {
172
+ if (error instanceof SdkError) {
173
+ throw error;
174
+ }
175
+ if (error instanceof TransportError && error.retryable && attempts < maxAttempts) {
176
+ continue;
177
+ }
178
+ if (error instanceof TransportError) {
179
+ throw new SdkError(error.code, error.message);
180
+ }
181
+ throw error;
182
+ }
183
+ }
184
+
185
+ throw new SdkError("transport_error", "exhausted retries");
186
+ }
187
+ }
188
+
189
+ function installLogBrewConsoleCapture(config) {
190
+ if (!config || typeof config !== "object") {
191
+ throw new SdkError("validation_error", "console capture config must be an object");
192
+ }
193
+
194
+ const client = config.client;
195
+ if (!(client instanceof LogBrewClient)) {
196
+ throw new SdkError("validation_error", "console capture client must be a LogBrewClient");
197
+ }
198
+
199
+ const targetConsole = config.console ?? globalThis.console;
200
+ if (!targetConsole || typeof targetConsole !== "object") {
201
+ throw new SdkError("validation_error", "console capture target must be an object");
202
+ }
203
+
204
+ const transport = config.transport;
205
+ const flushOnCapture = config.flushOnCapture === true;
206
+ const includeErrorStack = config.includeErrorStack === true;
207
+ const logger = config.logger ?? "console";
208
+ const metadata = compactMetadata(config.metadata);
209
+ const timestamp = typeof config.timestamp === "function"
210
+ ? config.timestamp
211
+ : () => new Date().toISOString();
212
+ const eventIdPrefix = config.eventIdPrefix ?? "console";
213
+ const onError = typeof config.onError === "function" ? config.onError : () => {};
214
+ const levels = normalizeConsoleLevels(config.levels);
215
+ const originals = new Map();
216
+ const state = {
217
+ installed: true,
218
+ captured: 0,
219
+ pendingFlush: Promise.resolve(null)
220
+ };
221
+
222
+ for (const method of levels) {
223
+ const original = targetConsole[method];
224
+ if (typeof original !== "function") {
225
+ continue;
226
+ }
227
+ originals.set(method, original);
228
+ targetConsole[method] = createConsoleCaptureMethod({
229
+ client,
230
+ eventIdPrefix,
231
+ flushOnCapture,
232
+ includeErrorStack,
233
+ logger,
234
+ metadata,
235
+ method,
236
+ onError,
237
+ original,
238
+ state,
239
+ timestamp,
240
+ transport
241
+ });
242
+ }
243
+
244
+ return {
245
+ async flush() {
246
+ if (transport && client.pendingEvents() > 0) {
247
+ state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
248
+ onError(error);
249
+ return null;
250
+ });
251
+ }
252
+ return state.pendingFlush;
253
+ },
254
+ uninstall() {
255
+ if (!state.installed) {
256
+ return;
257
+ }
258
+ state.installed = false;
259
+ for (const [method, original] of originals.entries()) {
260
+ targetConsole[method] = original;
261
+ }
262
+ originals.clear();
263
+ }
264
+ };
265
+ }
266
+
267
+ function createConsoleCaptureMethod(config) {
268
+ return function logBrewConsoleMethod(...args) {
269
+ config.original.apply(this, args);
270
+ if (!config.state.installed) {
271
+ return;
272
+ }
273
+ try {
274
+ config.state.captured += 1;
275
+ config.client.log(
276
+ `${config.eventIdPrefix}_${config.state.captured}`,
277
+ config.timestamp(),
278
+ logAttributesFromConsoleArgs(config.method, args, {
279
+ includeErrorStack: config.includeErrorStack,
280
+ logger: config.logger,
281
+ metadata: config.metadata
282
+ })
283
+ );
284
+ if (config.flushOnCapture && config.transport) {
285
+ config.state.pendingFlush = Promise.resolve(config.client.flush(config.transport)).catch((error) => {
286
+ config.onError(error);
287
+ return null;
288
+ });
289
+ }
290
+ } catch (error) {
291
+ config.onError(error);
292
+ }
293
+ };
294
+ }
295
+
296
+ function logAttributesFromConsoleArgs(method, args, options = {}) {
297
+ const logLevel = logbrewLevelFromConsoleMethod(method);
298
+ const includeErrorStack = options.includeErrorStack === true;
299
+ const message = consoleMessage(args, includeErrorStack);
300
+ const metadata = {
301
+ ...compactMetadata(options.metadata),
302
+ consoleMethod: method,
303
+ argumentCount: Array.isArray(args) ? args.length : 0
304
+ };
305
+ for (const value of Array.isArray(args) ? args : []) {
306
+ if (value instanceof Error) {
307
+ metadata.errorName = value.name || "Error";
308
+ if (value.message) {
309
+ metadata.errorMessage = value.message;
310
+ }
311
+ if (includeErrorStack && value.stack) {
312
+ metadata.errorStack = value.stack;
313
+ }
314
+ break;
315
+ }
316
+ }
317
+
318
+ return {
319
+ message,
320
+ level: logLevel,
321
+ ...(options.logger ? { logger: options.logger } : {}),
322
+ metadata
323
+ };
324
+ }
325
+
326
+ function logbrewLevelFromConsoleMethod(method) {
327
+ switch (method) {
328
+ case "debug":
329
+ return "debug";
330
+ case "warn":
331
+ return "warning";
332
+ case "error":
333
+ return "error";
334
+ case "info":
335
+ case "log":
336
+ return "info";
337
+ default:
338
+ throw new SdkError("validation_error", `console method must be one of: ${Array.from(CONSOLE_METHODS).join(", ")}`);
339
+ }
340
+ }
341
+
342
+ function parseTraceparent(traceparent) {
343
+ if (typeof traceparent !== "string" || traceparent.trim() === "") {
344
+ throw new SdkError("validation_error", "traceparent must be non-empty");
345
+ }
346
+
347
+ const match = TRACEPARENT_PATTERN.exec(traceparent.trim());
348
+ if (!match) {
349
+ throw new SdkError("validation_error", "traceparent must use W3C version-traceId-parentSpanId-traceFlags format");
350
+ }
351
+
352
+ const version = match[1].toLowerCase();
353
+ const traceId = match[2].toLowerCase();
354
+ const parentSpanId = match[3].toLowerCase();
355
+ const traceFlags = match[4].toLowerCase();
356
+ if (version === "ff") {
357
+ throw new SdkError("validation_error", "traceparent version ff is not allowed");
358
+ }
359
+ if (traceId === ZERO_TRACE_ID) {
360
+ throw new SdkError("validation_error", "traceparent traceId must not be all zeros");
361
+ }
362
+ if (parentSpanId === ZERO_SPAN_ID) {
363
+ throw new SdkError("validation_error", "traceparent parentSpanId must not be all zeros");
364
+ }
365
+
366
+ return {
367
+ version,
368
+ traceId,
369
+ parentSpanId,
370
+ traceFlags,
371
+ sampled: (Number.parseInt(traceFlags, 16) & 1) === 1
372
+ };
373
+ }
374
+
375
+ function createTraceparent({ traceId, spanId, traceFlags = "01" }) {
376
+ requireTraceId(traceId);
377
+ requireSpanId("spanId", spanId);
378
+ requireTraceFlags(traceFlags);
379
+ return `00-${traceId.toLowerCase()}-${spanId.toLowerCase()}-${traceFlags.toLowerCase()}`;
380
+ }
381
+
382
+ function spanAttributesFromTraceparent(traceparent, attributes) {
383
+ if (!attributes || Array.isArray(attributes) || typeof attributes !== "object") {
384
+ throw new SdkError("validation_error", "span attributes must be an object");
385
+ }
386
+ const context = parseTraceparent(traceparent);
387
+ requireNonEmpty("span name", attributes.name);
388
+ requireSpanId("spanId", attributes.spanId);
389
+ requireAllowedValue("span status", attributes.status, SPAN_STATUSES);
390
+ if (attributes.durationMs !== undefined) {
391
+ if (typeof attributes.durationMs !== "number" || Number.isNaN(attributes.durationMs) || attributes.durationMs < 0) {
392
+ throw new SdkError("validation_error", "span durationMs must be non-negative");
393
+ }
394
+ }
395
+
396
+ return {
397
+ name: attributes.name,
398
+ traceId: context.traceId,
399
+ spanId: attributes.spanId.toLowerCase(),
400
+ parentSpanId: context.parentSpanId,
401
+ status: attributes.status,
402
+ ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
403
+ ...(attributes.metadata !== undefined ? { metadata: compactMetadata(attributes.metadata) } : {})
404
+ };
405
+ }
406
+
407
+ function createLogBrewPinoDestination(config) {
408
+ if (!config || typeof config !== "object") {
409
+ throw new SdkError("validation_error", "Pino destination config must be an object");
410
+ }
411
+
412
+ const client = config.client;
413
+ if (!(client instanceof LogBrewClient)) {
414
+ throw new SdkError("validation_error", "Pino destination client must be a LogBrewClient");
415
+ }
416
+
417
+ const transport = config.transport;
418
+ const flushOnWrite = config.flushOnWrite === true;
419
+ const includeErrorStack = config.includeErrorStack === true;
420
+ const logger = config.logger ?? "pino";
421
+ const metadata = compactMetadata(config.metadata);
422
+ const timestamp = typeof config.timestamp === "function"
423
+ ? config.timestamp
424
+ : () => new Date().toISOString();
425
+ const eventIdPrefix = config.eventIdPrefix ?? "pino";
426
+ const onError = typeof config.onError === "function" ? config.onError : () => {};
427
+ const state = {
428
+ captured: 0,
429
+ pendingFlush: Promise.resolve(null)
430
+ };
431
+
432
+ return {
433
+ write(chunk) {
434
+ const lines = String(chunk).split(/\r?\n/u).filter((line) => line.trim() !== "");
435
+ for (const line of lines) {
436
+ try {
437
+ const record = JSON.parse(line);
438
+ state.captured += 1;
439
+ client.log(
440
+ `${eventIdPrefix}_${state.captured}`,
441
+ timestampFromPinoRecord(record, timestamp),
442
+ logAttributesFromPinoRecord(record, {
443
+ includeErrorStack,
444
+ logger,
445
+ metadata
446
+ })
447
+ );
448
+ if (flushOnWrite && transport) {
449
+ state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
450
+ onError(error);
451
+ return null;
452
+ });
453
+ }
454
+ } catch (error) {
455
+ onError(error);
456
+ }
457
+ }
458
+ return true;
459
+ },
460
+ async flush() {
461
+ if (transport && client.pendingEvents() > 0) {
462
+ state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
463
+ onError(error);
464
+ return null;
465
+ });
466
+ }
467
+ return state.pendingFlush;
468
+ },
469
+ end() {
470
+ return this.flush();
471
+ }
472
+ };
473
+ }
474
+
475
+ function logAttributesFromPinoRecord(record, options = {}) {
476
+ if (!record || Array.isArray(record) || typeof record !== "object") {
477
+ throw new SdkError("validation_error", "Pino record must be an object");
478
+ }
479
+
480
+ const level = logbrewLevelFromPinoLevel(record.level);
481
+ const metadata = {
482
+ ...compactMetadata(options.metadata),
483
+ pinoLevel: pinoLevelLabel(record.level),
484
+ ...pinoContextMetadata(record)
485
+ };
486
+ if (typeof record.level === "number" && Number.isFinite(record.level)) {
487
+ metadata.pinoLevelNumber = record.level;
488
+ }
489
+ addPinoErrorMetadata(metadata, record.err ?? record.error, options.includeErrorStack === true);
490
+
491
+ return {
492
+ message: pinoMessage(record),
493
+ level,
494
+ ...(options.logger ? { logger: options.logger } : {}),
495
+ metadata
496
+ };
497
+ }
498
+
499
+ function createLogBrewWinstonTransport(config) {
500
+ if (!config || typeof config !== "object") {
501
+ throw new SdkError("validation_error", "Winston transport config must be an object");
502
+ }
503
+
504
+ const client = config.client;
505
+ if (!(client instanceof LogBrewClient)) {
506
+ throw new SdkError("validation_error", "Winston transport client must be a LogBrewClient");
507
+ }
508
+
509
+ const transport = config.transport;
510
+ const flushOnWrite = config.flushOnWrite === true;
511
+ const includeErrorStack = config.includeErrorStack === true;
512
+ const logger = config.logger ?? "winston";
513
+ const metadata = compactMetadata(config.metadata);
514
+ const timestamp = typeof config.timestamp === "function"
515
+ ? config.timestamp
516
+ : () => new Date().toISOString();
517
+ const eventIdPrefix = config.eventIdPrefix ?? "winston";
518
+ const onError = typeof config.onError === "function" ? config.onError : () => {};
519
+ const state = {
520
+ captured: 0,
521
+ pendingFlush: Promise.resolve(null)
522
+ };
523
+ const { Writable } = require("node:stream");
524
+
525
+ const winstonTransport = new Writable({
526
+ objectMode: true,
527
+ write(info, _encoding, callback) {
528
+ try {
529
+ captureWinstonInfo({
530
+ client,
531
+ eventIdPrefix,
532
+ flushOnWrite,
533
+ includeErrorStack,
534
+ info,
535
+ logger,
536
+ metadata,
537
+ onError,
538
+ state,
539
+ timestamp,
540
+ transport
541
+ });
542
+ } catch (error) {
543
+ onError(error);
544
+ } finally {
545
+ callback();
546
+ }
547
+ }
548
+ });
549
+
550
+ winstonTransport.log = function log(info, callback) {
551
+ this.write(info);
552
+ if (typeof callback === "function") {
553
+ callback();
554
+ }
555
+ };
556
+ winstonTransport.flush = async () => {
557
+ if (transport && client.pendingEvents() > 0) {
558
+ state.pendingFlush = Promise.resolve(client.flush(transport)).catch((error) => {
559
+ onError(error);
560
+ return null;
561
+ });
562
+ }
563
+ return state.pendingFlush;
564
+ };
565
+ if (typeof config.level === "string" && config.level.trim() !== "") {
566
+ winstonTransport.level = config.level;
567
+ }
568
+ if (config.name !== undefined) {
569
+ winstonTransport.name = String(config.name);
570
+ }
571
+ if (config.silent === true) {
572
+ winstonTransport.silent = true;
573
+ }
574
+ if (config.handleExceptions === true) {
575
+ winstonTransport.handleExceptions = true;
576
+ }
577
+ if (config.handleRejections === true) {
578
+ winstonTransport.handleRejections = true;
579
+ }
580
+
581
+ return winstonTransport;
582
+ }
583
+
584
+ function captureWinstonInfo(config) {
585
+ if (config.info?.silent === true) {
586
+ return;
587
+ }
588
+ config.state.captured += 1;
589
+ config.client.log(
590
+ `${config.eventIdPrefix}_${config.state.captured}`,
591
+ timestampFromWinstonInfo(config.info, config.timestamp),
592
+ logAttributesFromWinstonInfo(config.info, {
593
+ includeErrorStack: config.includeErrorStack,
594
+ logger: config.logger,
595
+ metadata: config.metadata
596
+ })
597
+ );
598
+ if (config.flushOnWrite && config.transport) {
599
+ config.state.pendingFlush = Promise.resolve(config.client.flush(config.transport)).catch((error) => {
600
+ config.onError(error);
601
+ return null;
602
+ });
603
+ }
604
+ }
605
+
606
+ function logAttributesFromWinstonInfo(info, options = {}) {
607
+ if (!info || Array.isArray(info) || typeof info !== "object") {
608
+ throw new SdkError("validation_error", "Winston info must be an object");
609
+ }
610
+
611
+ const level = logbrewLevelFromWinstonLevel(info.level);
612
+ const metadata = {
613
+ ...compactMetadata(options.metadata),
614
+ winstonLevel: winstonLevelLabel(info.level),
615
+ ...winstonContextMetadata(info)
616
+ };
617
+ addWinstonErrorMetadata(metadata, info, options.includeErrorStack === true);
618
+
619
+ return {
620
+ message: winstonMessage(info),
621
+ level,
622
+ ...(options.logger ? { logger: options.logger } : {}),
623
+ metadata
624
+ };
625
+ }
626
+
627
+ function timestampFromWinstonInfo(info, fallbackTimestamp) {
628
+ const value = info?.timestamp ?? info?.time;
629
+ if (typeof value === "number" && Number.isFinite(value)) {
630
+ return new Date(value).toISOString();
631
+ }
632
+ if (typeof value === "string" && value.trim() !== "") {
633
+ const parsed = new Date(value);
634
+ if (!Number.isNaN(parsed.valueOf())) {
635
+ return parsed.toISOString();
636
+ }
637
+ return value;
638
+ }
639
+ if (value instanceof Date && !Number.isNaN(value.valueOf())) {
640
+ return value.toISOString();
641
+ }
642
+ return fallbackTimestamp();
643
+ }
644
+
645
+ function logbrewLevelFromWinstonLevel(level) {
646
+ switch (String(level).toLowerCase()) {
647
+ case "debug":
648
+ case "silly":
649
+ return "debug";
650
+ case "warn":
651
+ case "warning":
652
+ return "warning";
653
+ case "error":
654
+ case "crit":
655
+ case "critical":
656
+ case "fatal":
657
+ return "error";
658
+ case "http":
659
+ case "verbose":
660
+ case "info":
661
+ default:
662
+ return "info";
663
+ }
664
+ }
665
+
666
+ function winstonLevelLabel(level) {
667
+ return typeof level === "string" && level.trim() !== "" ? level : "info";
668
+ }
669
+
670
+ function winstonMessage(info) {
671
+ if (typeof info.message === "string" && info.message.trim() !== "") {
672
+ return info.message;
673
+ }
674
+ const error = info.err ?? info.error;
675
+ if (error && typeof error === "object" && typeof error.message === "string" && error.message.trim() !== "") {
676
+ return error.message;
677
+ }
678
+ return "winston event";
679
+ }
680
+
681
+ function winstonContextMetadata(info) {
682
+ const metadata = {};
683
+ for (const [key, value] of Object.entries(info)) {
684
+ if (!WINSTON_RESERVED_FIELDS.has(key) && isMetadataValue(value)) {
685
+ metadata[`context.${key}`] = value;
686
+ }
687
+ }
688
+ return metadata;
689
+ }
690
+
691
+ function addWinstonErrorMetadata(metadata, info, includeErrorStack) {
692
+ const error = info.err ?? info.error;
693
+ addWinstonNestedErrorMetadata(metadata, error, includeErrorStack);
694
+ if (typeof info.stack === "string" && info.stack.trim() !== "") {
695
+ const firstLine = info.stack.split(/\r?\n/u)[0] ?? "";
696
+ const match = /^([A-Za-z][A-Za-z0-9_.]*(?:Error|Exception)?):\s*(.*)$/u.exec(firstLine);
697
+ if (match && metadata.errorName === undefined) {
698
+ metadata.errorName = match[1];
699
+ }
700
+ if (match && match[2] && metadata.errorMessage === undefined) {
701
+ metadata.errorMessage = match[2];
702
+ }
703
+ if (includeErrorStack) {
704
+ metadata.errorStack = info.stack;
705
+ }
706
+ }
707
+ }
708
+
709
+ function addWinstonNestedErrorMetadata(metadata, error, includeErrorStack) {
710
+ if (!error) {
711
+ return;
712
+ }
713
+ if (error instanceof Error) {
714
+ metadata.errorName = error.name || "Error";
715
+ if (error.message) {
716
+ metadata.errorMessage = error.message;
717
+ }
718
+ if (includeErrorStack && error.stack) {
719
+ metadata.errorStack = error.stack;
720
+ }
721
+ return;
722
+ }
723
+ if (typeof error === "object") {
724
+ const name = error.type ?? error.name;
725
+ const message = error.message;
726
+ const stack = error.stack;
727
+ if (typeof name === "string" && name.trim() !== "") {
728
+ metadata.errorName = name;
729
+ }
730
+ if (typeof message === "string" && message.trim() !== "") {
731
+ metadata.errorMessage = message;
732
+ }
733
+ if (includeErrorStack && typeof stack === "string" && stack.trim() !== "") {
734
+ metadata.errorStack = stack;
735
+ }
736
+ return;
737
+ }
738
+ if (typeof error === "string" && error.trim() !== "") {
739
+ metadata.errorMessage = error;
740
+ }
741
+ }
742
+
743
+ function timestampFromPinoRecord(record, fallbackTimestamp) {
744
+ const value = record?.time ?? record?.timestamp;
745
+ if (typeof value === "number" && Number.isFinite(value)) {
746
+ return new Date(value).toISOString();
747
+ }
748
+ if (typeof value === "string" && value.trim() !== "") {
749
+ const parsed = new Date(value);
750
+ if (!Number.isNaN(parsed.valueOf())) {
751
+ return parsed.toISOString();
752
+ }
753
+ return value;
754
+ }
755
+ return fallbackTimestamp();
756
+ }
757
+
758
+ function logbrewLevelFromPinoLevel(level) {
759
+ if (typeof level === "number" && Number.isFinite(level)) {
760
+ if (level >= 50) {
761
+ return "error";
762
+ }
763
+ if (level >= 40) {
764
+ return "warning";
765
+ }
766
+ if (level >= 30) {
767
+ return "info";
768
+ }
769
+ return "debug";
770
+ }
771
+
772
+ switch (String(level).toLowerCase()) {
773
+ case "trace":
774
+ case "debug":
775
+ return "debug";
776
+ case "warn":
777
+ case "warning":
778
+ return "warning";
779
+ case "error":
780
+ case "fatal":
781
+ return "error";
782
+ case "info":
783
+ default:
784
+ return "info";
785
+ }
786
+ }
787
+
788
+ function pinoLevelLabel(level) {
789
+ if (typeof level === "string" && level.trim() !== "") {
790
+ return level;
791
+ }
792
+ switch (level) {
793
+ case 10:
794
+ return "trace";
795
+ case 20:
796
+ return "debug";
797
+ case 30:
798
+ return "info";
799
+ case 40:
800
+ return "warn";
801
+ case 50:
802
+ return "error";
803
+ case 60:
804
+ return "fatal";
805
+ default:
806
+ return typeof level === "number" && Number.isFinite(level) ? String(level) : "info";
807
+ }
808
+ }
809
+
810
+ function pinoMessage(record) {
811
+ if (typeof record.msg === "string" && record.msg.trim() !== "") {
812
+ return record.msg;
813
+ }
814
+ if (typeof record.message === "string" && record.message.trim() !== "") {
815
+ return record.message;
816
+ }
817
+ const error = record.err ?? record.error;
818
+ if (error && typeof error === "object" && typeof error.message === "string" && error.message.trim() !== "") {
819
+ return error.message;
820
+ }
821
+ return "pino event";
822
+ }
823
+
824
+ function pinoContextMetadata(record) {
825
+ const metadata = {};
826
+ for (const [key, value] of Object.entries(record)) {
827
+ if (!PINO_RESERVED_FIELDS.has(key) && isMetadataValue(value)) {
828
+ metadata[`context.${key}`] = value;
829
+ }
830
+ }
831
+ return metadata;
832
+ }
833
+
834
+ function addPinoErrorMetadata(metadata, error, includeErrorStack) {
835
+ if (!error) {
836
+ return;
837
+ }
838
+ if (error instanceof Error) {
839
+ metadata.errorName = error.name || "Error";
840
+ if (error.message) {
841
+ metadata.errorMessage = error.message;
842
+ }
843
+ if (includeErrorStack && error.stack) {
844
+ metadata.errorStack = error.stack;
845
+ }
846
+ return;
847
+ }
848
+ if (typeof error === "object") {
849
+ const name = error.type ?? error.name;
850
+ const message = error.message;
851
+ const stack = error.stack;
852
+ if (typeof name === "string" && name.trim() !== "") {
853
+ metadata.errorName = name;
854
+ }
855
+ if (typeof message === "string" && message.trim() !== "") {
856
+ metadata.errorMessage = message;
857
+ }
858
+ if (includeErrorStack && typeof stack === "string" && stack.trim() !== "") {
859
+ metadata.errorStack = stack;
860
+ }
861
+ return;
862
+ }
863
+ if (typeof error === "string" && error.trim() !== "") {
864
+ metadata.errorMessage = error;
865
+ }
866
+ }
867
+
868
+ function requireNonEmpty(label, value) {
869
+ if (typeof value !== "string" || value.trim() === "") {
870
+ throw new SdkError("validation_error", `${label} must be non-empty`);
871
+ }
872
+ }
873
+
874
+ function requireAllowedValue(label, value, allowedValues) {
875
+ requireNonEmpty(label, value);
876
+ if (!allowedValues.has(value)) {
877
+ throw new SdkError(
878
+ "validation_error",
879
+ `${label} must be one of: ${Array.from(allowedValues).join(", ")}`
880
+ );
881
+ }
882
+ }
883
+
884
+ function requireTraceId(traceId) {
885
+ if (typeof traceId !== "string" || !/^[0-9a-fA-F]{32}$/u.test(traceId)) {
886
+ throw new SdkError("validation_error", "traceId must be 32 lowercase or uppercase hex characters");
887
+ }
888
+ if (traceId.toLowerCase() === ZERO_TRACE_ID) {
889
+ throw new SdkError("validation_error", "traceId must not be all zeros");
890
+ }
891
+ }
892
+
893
+ function requireSpanId(label, spanId) {
894
+ if (typeof spanId !== "string" || !/^[0-9a-fA-F]{16}$/u.test(spanId)) {
895
+ throw new SdkError("validation_error", `${label} must be 16 lowercase or uppercase hex characters`);
896
+ }
897
+ if (spanId.toLowerCase() === ZERO_SPAN_ID) {
898
+ throw new SdkError("validation_error", `${label} must not be all zeros`);
899
+ }
900
+ }
901
+
902
+ function requireTraceFlags(traceFlags) {
903
+ if (typeof traceFlags !== "string" || !/^[0-9a-fA-F]{2}$/u.test(traceFlags)) {
904
+ throw new SdkError("validation_error", "traceFlags must be 2 lowercase or uppercase hex characters");
905
+ }
906
+ }
907
+
908
+ function requireTimestamp(timestamp) {
909
+ requireNonEmpty("timestamp", timestamp);
910
+ if (timestamp.endsWith("Z")) {
911
+ return;
912
+ }
913
+ const timePortion = timestamp.split("T")[1];
914
+ if (timePortion && (timePortion.includes("+") || /.+-.+/.test(timePortion))) {
915
+ return;
916
+ }
917
+ throw new SdkError(
918
+ "validation_error",
919
+ `timestamp must include a timezone offset: ${timestamp}`
920
+ );
921
+ }
922
+
923
+ function cloneMetadata(metadata) {
924
+ if (metadata === undefined) {
925
+ return undefined;
926
+ }
927
+ if (!metadata || Array.isArray(metadata) || typeof metadata !== "object") {
928
+ throw new SdkError("validation_error", "metadata must be an object");
929
+ }
930
+ return { ...metadata };
931
+ }
932
+
933
+ function validateRelease(attributes) {
934
+ requireNonEmpty("release version", attributes.version);
935
+ if (attributes.commit !== undefined) {
936
+ requireNonEmpty("release commit", attributes.commit);
937
+ }
938
+ return withMetadata({
939
+ version: attributes.version,
940
+ ...(attributes.commit ? { commit: attributes.commit } : {}),
941
+ ...(attributes.notes !== undefined ? { notes: attributes.notes } : {})
942
+ }, attributes.metadata);
943
+ }
944
+
945
+ function validateEnvironment(attributes) {
946
+ requireNonEmpty("environment name", attributes.name);
947
+ return withMetadata({
948
+ name: attributes.name,
949
+ ...(attributes.region !== undefined ? { region: attributes.region } : {})
950
+ }, attributes.metadata);
951
+ }
952
+
953
+ function validateIssue(attributes) {
954
+ requireNonEmpty("issue title", attributes.title);
955
+ requireAllowedValue("issue level", attributes.level, ISSUE_LEVELS);
956
+ return withMetadata({
957
+ title: attributes.title,
958
+ level: attributes.level,
959
+ ...(attributes.message !== undefined ? { message: attributes.message } : {})
960
+ }, attributes.metadata);
961
+ }
962
+
963
+ function validateLog(attributes) {
964
+ requireNonEmpty("log message", attributes.message);
965
+ requireAllowedValue("log level", attributes.level, LOG_LEVELS);
966
+ return withMetadata({
967
+ message: attributes.message,
968
+ level: attributes.level,
969
+ ...(attributes.logger !== undefined ? { logger: attributes.logger } : {})
970
+ }, attributes.metadata);
971
+ }
972
+
973
+ function validateSpan(attributes) {
974
+ requireNonEmpty("span name", attributes.name);
975
+ requireNonEmpty("span traceId", attributes.traceId);
976
+ requireNonEmpty("span spanId", attributes.spanId);
977
+ requireAllowedValue("span status", attributes.status, SPAN_STATUSES);
978
+ if (attributes.parentSpanId !== undefined) {
979
+ requireNonEmpty("span parentSpanId", attributes.parentSpanId);
980
+ }
981
+ if (attributes.durationMs !== undefined) {
982
+ if (typeof attributes.durationMs !== "number" || Number.isNaN(attributes.durationMs) || attributes.durationMs < 0) {
983
+ throw new SdkError("validation_error", "span durationMs must be non-negative");
984
+ }
985
+ }
986
+ return withMetadata({
987
+ name: attributes.name,
988
+ traceId: attributes.traceId,
989
+ spanId: attributes.spanId,
990
+ status: attributes.status,
991
+ ...(attributes.parentSpanId !== undefined ? { parentSpanId: attributes.parentSpanId } : {}),
992
+ ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {})
993
+ }, attributes.metadata);
994
+ }
995
+
996
+ function validateAction(attributes) {
997
+ requireNonEmpty("action name", attributes.name);
998
+ requireAllowedValue("action status", attributes.status, ACTION_STATUSES);
999
+ return withMetadata({
1000
+ name: attributes.name,
1001
+ status: attributes.status
1002
+ }, attributes.metadata);
1003
+ }
1004
+
1005
+ function withMetadata(attributes, metadata) {
1006
+ const safeMetadata = cloneMetadata(metadata);
1007
+ return safeMetadata === undefined
1008
+ ? attributes
1009
+ : { ...attributes, metadata: safeMetadata };
1010
+ }
1011
+
1012
+ function compactMetadata(metadata) {
1013
+ if (metadata === undefined) {
1014
+ return {};
1015
+ }
1016
+ if (!metadata || Array.isArray(metadata) || typeof metadata !== "object") {
1017
+ throw new SdkError("validation_error", "metadata must be an object");
1018
+ }
1019
+ const safeMetadata = {};
1020
+ for (const [key, value] of Object.entries(metadata)) {
1021
+ if (isMetadataValue(value)) {
1022
+ safeMetadata[key] = value;
1023
+ }
1024
+ }
1025
+ return safeMetadata;
1026
+ }
1027
+
1028
+ function isMetadataValue(value) {
1029
+ return (
1030
+ value === null
1031
+ || typeof value === "string"
1032
+ || typeof value === "number" && Number.isFinite(value)
1033
+ || typeof value === "boolean"
1034
+ );
1035
+ }
1036
+
1037
+ function normalizeConsoleLevels(levels) {
1038
+ const requestedLevels = levels === undefined ? DEFAULT_CONSOLE_LEVELS : levels;
1039
+ if (!Array.isArray(requestedLevels)) {
1040
+ throw new SdkError("validation_error", "console capture levels must be an array");
1041
+ }
1042
+ const normalized = [];
1043
+ for (const method of requestedLevels) {
1044
+ if (!CONSOLE_METHODS.has(method)) {
1045
+ throw new SdkError("validation_error", `console method must be one of: ${Array.from(CONSOLE_METHODS).join(", ")}`);
1046
+ }
1047
+ if (!normalized.includes(method)) {
1048
+ normalized.push(method);
1049
+ }
1050
+ }
1051
+ return normalized;
1052
+ }
1053
+
1054
+ function consoleMessage(args, includeErrorStack) {
1055
+ const values = Array.isArray(args) ? args : [];
1056
+ const message = values.map((value) => formatConsoleArgument(value, includeErrorStack)).join(" ");
1057
+ return message.trim() === "" ? "console event" : message;
1058
+ }
1059
+
1060
+ function formatConsoleArgument(value, includeErrorStack) {
1061
+ if (value instanceof Error) {
1062
+ if (includeErrorStack && value.stack) {
1063
+ return value.stack;
1064
+ }
1065
+ return value.message ? `${value.name}: ${value.message}` : value.name;
1066
+ }
1067
+ if (typeof value === "string") {
1068
+ return value;
1069
+ }
1070
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === undefined) {
1071
+ return String(value);
1072
+ }
1073
+ if (typeof value === "bigint" || typeof value === "symbol") {
1074
+ return String(value);
1075
+ }
1076
+ try {
1077
+ const json = JSON.stringify(value);
1078
+ return json === undefined ? String(value) : json;
1079
+ } catch {
1080
+ return Object.prototype.toString.call(value);
1081
+ }
1082
+ }
1083
+
1084
+ module.exports = {
1085
+ createTraceparent,
1086
+ createLogBrewPinoDestination,
1087
+ createLogBrewWinstonTransport,
1088
+ installLogBrewConsoleCapture,
1089
+ LogBrewClient,
1090
+ logAttributesFromConsoleArgs,
1091
+ logAttributesFromPinoRecord,
1092
+ logAttributesFromWinstonInfo,
1093
+ logbrewLevelFromConsoleMethod,
1094
+ parseTraceparent,
1095
+ RecordingTransport,
1096
+ SdkError,
1097
+ spanAttributesFromTraceparent,
1098
+ TransportError
1099
+ };