voltlog-io 1.0.1 → 1.0.3
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 -21
- package/README.md +316 -65
- package/dist/index.d.mts +374 -5
- package/dist/index.d.ts +374 -5
- package/dist/index.js +708 -247
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +592 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +75 -78
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
12
|
+
var __filename = /* @__PURE__ */ getFilename();
|
|
13
|
+
|
|
1
14
|
// src/core/types.ts
|
|
2
15
|
var LogLevel = {
|
|
3
16
|
TRACE: 10,
|
|
@@ -29,7 +42,7 @@ function shouldIncludeStack(entryLevel, includeStack) {
|
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
// src/core/logger.ts
|
|
32
|
-
import {
|
|
45
|
+
import { randomUUID } from "crypto";
|
|
33
46
|
|
|
34
47
|
// src/core/pipeline.ts
|
|
35
48
|
function composeMiddleware(middleware, final) {
|
|
@@ -71,6 +84,7 @@ var LoggerImpl = class {
|
|
|
71
84
|
_context;
|
|
72
85
|
_includeStack;
|
|
73
86
|
_timestampFn;
|
|
87
|
+
_idFn;
|
|
74
88
|
constructor(options = {}) {
|
|
75
89
|
this._level = resolveLevel(options.level ?? "INFO");
|
|
76
90
|
this._transports = [...options.transports ?? []];
|
|
@@ -78,22 +92,28 @@ var LoggerImpl = class {
|
|
|
78
92
|
this._context = options.context ? { ...options.context } : {};
|
|
79
93
|
this._includeStack = options.includeStack ?? "ERROR";
|
|
80
94
|
this._timestampFn = options.timestamp ?? Date.now;
|
|
95
|
+
this._idFn = options.idGenerator !== void 0 ? options.idGenerator : randomUUID;
|
|
81
96
|
this._pipeline = this._buildPipeline();
|
|
82
97
|
}
|
|
83
98
|
// ─── Log Methods ────────────────────────────────────────────
|
|
84
99
|
trace(message, meta) {
|
|
100
|
+
if (10 < this._level) return;
|
|
85
101
|
this._log(10, "TRACE", message, meta);
|
|
86
102
|
}
|
|
87
103
|
debug(message, meta) {
|
|
104
|
+
if (20 < this._level) return;
|
|
88
105
|
this._log(20, "DEBUG", message, meta);
|
|
89
106
|
}
|
|
90
107
|
info(message, meta) {
|
|
108
|
+
if (30 < this._level) return;
|
|
91
109
|
this._log(30, "INFO", message, meta);
|
|
92
110
|
}
|
|
93
111
|
warn(message, meta) {
|
|
112
|
+
if (40 < this._level) return;
|
|
94
113
|
this._log(40, "WARN", message, meta);
|
|
95
114
|
}
|
|
96
115
|
error(message, metaOrError, error) {
|
|
116
|
+
if (50 < this._level) return;
|
|
97
117
|
if (metaOrError instanceof Error) {
|
|
98
118
|
this._log(50, "ERROR", message, void 0, metaOrError);
|
|
99
119
|
} else {
|
|
@@ -122,6 +142,34 @@ var LoggerImpl = class {
|
|
|
122
142
|
this._middlewareList.push(middleware);
|
|
123
143
|
this._pipeline = this._buildPipeline();
|
|
124
144
|
}
|
|
145
|
+
removeMiddleware(middleware) {
|
|
146
|
+
this._middlewareList = this._middlewareList.filter((m) => m !== middleware);
|
|
147
|
+
this._pipeline = this._buildPipeline();
|
|
148
|
+
}
|
|
149
|
+
// ─── Level Control ─────────────────────────────────────────
|
|
150
|
+
setLevel(level) {
|
|
151
|
+
this._level = resolveLevel(level);
|
|
152
|
+
}
|
|
153
|
+
getLevel() {
|
|
154
|
+
return LogLevelValueMap[this._level] ?? "INFO";
|
|
155
|
+
}
|
|
156
|
+
isLevelEnabled(level) {
|
|
157
|
+
return resolveLevel(level) >= this._level;
|
|
158
|
+
}
|
|
159
|
+
// ─── Timer ─────────────────────────────────────────────────
|
|
160
|
+
startTimer(level) {
|
|
161
|
+
const start = performance.now();
|
|
162
|
+
const logLevel = level ?? "INFO";
|
|
163
|
+
return {
|
|
164
|
+
done: (message, meta) => {
|
|
165
|
+
const durationMs = Math.round(performance.now() - start);
|
|
166
|
+
const merged = { ...meta, durationMs };
|
|
167
|
+
const methodKey = logLevel.toLowerCase();
|
|
168
|
+
this[methodKey](message, merged);
|
|
169
|
+
},
|
|
170
|
+
elapsed: () => Math.round(performance.now() - start)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
125
173
|
// ─── Lifecycle ──────────────────────────────────────────────
|
|
126
174
|
async flush() {
|
|
127
175
|
await Promise.all(this._transports.map((t) => t.flush?.()).filter(Boolean));
|
|
@@ -139,7 +187,7 @@ var LoggerImpl = class {
|
|
|
139
187
|
_logWithContext(level, levelName, message, context, meta, error) {
|
|
140
188
|
if (!shouldLog(level, this._level)) return;
|
|
141
189
|
const entry = {
|
|
142
|
-
id:
|
|
190
|
+
id: this._idFn ? this._idFn() : "",
|
|
143
191
|
level,
|
|
144
192
|
levelName,
|
|
145
193
|
message,
|
|
@@ -148,15 +196,10 @@ var LoggerImpl = class {
|
|
|
148
196
|
context: Object.keys(context).length > 0 ? context : void 0
|
|
149
197
|
};
|
|
150
198
|
if (error) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
if (shouldIncludeStack(level, this._includeStack)) {
|
|
157
|
-
logError.stack = error.stack;
|
|
158
|
-
}
|
|
159
|
-
entry.error = logError;
|
|
199
|
+
entry.error = serializeError(
|
|
200
|
+
error,
|
|
201
|
+
shouldIncludeStack(level, this._includeStack)
|
|
202
|
+
);
|
|
160
203
|
}
|
|
161
204
|
this._pipeline(entry);
|
|
162
205
|
}
|
|
@@ -166,6 +209,20 @@ var LoggerImpl = class {
|
|
|
166
209
|
});
|
|
167
210
|
}
|
|
168
211
|
};
|
|
212
|
+
function serializeError(error, includeStack, depth = 0) {
|
|
213
|
+
const logError = {
|
|
214
|
+
message: error.message,
|
|
215
|
+
name: error.name,
|
|
216
|
+
code: error.code
|
|
217
|
+
};
|
|
218
|
+
if (includeStack) {
|
|
219
|
+
logError.stack = error.stack;
|
|
220
|
+
}
|
|
221
|
+
if (error.cause instanceof Error && depth < 5) {
|
|
222
|
+
logError.cause = serializeError(error.cause, includeStack, depth + 1);
|
|
223
|
+
}
|
|
224
|
+
return logError;
|
|
225
|
+
}
|
|
169
226
|
var ChildLoggerImpl = class _ChildLoggerImpl {
|
|
170
227
|
constructor(_parent, _context) {
|
|
171
228
|
this._parent = _parent;
|
|
@@ -240,6 +297,21 @@ var ChildLoggerImpl = class _ChildLoggerImpl {
|
|
|
240
297
|
addMiddleware(middleware) {
|
|
241
298
|
this._parent.addMiddleware(middleware);
|
|
242
299
|
}
|
|
300
|
+
removeMiddleware(middleware) {
|
|
301
|
+
this._parent.removeMiddleware(middleware);
|
|
302
|
+
}
|
|
303
|
+
setLevel(level) {
|
|
304
|
+
this._parent.setLevel(level);
|
|
305
|
+
}
|
|
306
|
+
getLevel() {
|
|
307
|
+
return this._parent.getLevel();
|
|
308
|
+
}
|
|
309
|
+
isLevelEnabled(level) {
|
|
310
|
+
return this._parent.isLevelEnabled(level);
|
|
311
|
+
}
|
|
312
|
+
startTimer(level) {
|
|
313
|
+
return this._parent.startTimer(level);
|
|
314
|
+
}
|
|
243
315
|
flush() {
|
|
244
316
|
return this._parent.flush();
|
|
245
317
|
}
|
|
@@ -354,11 +426,49 @@ function alertMiddleware(rules) {
|
|
|
354
426
|
};
|
|
355
427
|
}
|
|
356
428
|
|
|
429
|
+
// src/middleware/async-context.ts
|
|
430
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
431
|
+
function asyncContextMiddleware() {
|
|
432
|
+
const storage = new AsyncLocalStorage();
|
|
433
|
+
const middleware = (entry, next) => {
|
|
434
|
+
const ctx = storage.getStore();
|
|
435
|
+
if (ctx) {
|
|
436
|
+
const meta = entry.meta;
|
|
437
|
+
for (const [key, value] of Object.entries(ctx)) {
|
|
438
|
+
if (!(key in meta)) {
|
|
439
|
+
meta[key] = value;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (ctx.requestId && !entry.correlationId) {
|
|
443
|
+
entry.correlationId = String(ctx.requestId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
next(entry);
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
middleware,
|
|
450
|
+
runInContext(context, fn) {
|
|
451
|
+
const parentCtx = storage.getStore();
|
|
452
|
+
const mergedCtx = parentCtx ? { ...parentCtx, ...context } : { ...context };
|
|
453
|
+
storage.run(mergedCtx, fn);
|
|
454
|
+
},
|
|
455
|
+
getContext() {
|
|
456
|
+
return storage.getStore();
|
|
457
|
+
},
|
|
458
|
+
updateContext(updates) {
|
|
459
|
+
const ctx = storage.getStore();
|
|
460
|
+
if (ctx) {
|
|
461
|
+
Object.assign(ctx, updates);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
357
467
|
// src/middleware/correlation-id.ts
|
|
358
|
-
import {
|
|
468
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
359
469
|
function correlationIdMiddleware(options = {}) {
|
|
360
470
|
const header = options.header ?? "x-correlation-id";
|
|
361
|
-
const generate = options.generator ??
|
|
471
|
+
const generate = options.generator ?? randomUUID2;
|
|
362
472
|
return (entry, next) => {
|
|
363
473
|
if (entry.correlationId) {
|
|
364
474
|
return next(entry);
|
|
@@ -433,6 +543,71 @@ function heapUsageMiddleware(options = {}) {
|
|
|
433
543
|
};
|
|
434
544
|
}
|
|
435
545
|
|
|
546
|
+
// src/middleware/http.ts
|
|
547
|
+
var nodeHttpMappers = {
|
|
548
|
+
req: {
|
|
549
|
+
getMethod: (req) => req.method || "UNKNOWN",
|
|
550
|
+
getUrl: (req) => req.originalUrl || req.url || "/",
|
|
551
|
+
getIp: (req) => req.ip || req.socket?.remoteAddress || req.headers?.["x-forwarded-for"] || void 0,
|
|
552
|
+
getUserAgent: (req) => req.headers?.["user-agent"] || void 0,
|
|
553
|
+
getHeader: (req, name) => req.headers?.[name] || void 0
|
|
554
|
+
},
|
|
555
|
+
res: {
|
|
556
|
+
getStatusCode: (res) => res.statusCode || 200,
|
|
557
|
+
onFinish: (res, callback) => {
|
|
558
|
+
if (typeof res.on === "function") {
|
|
559
|
+
res.on("finish", callback);
|
|
560
|
+
res.on("close", callback);
|
|
561
|
+
} else {
|
|
562
|
+
callback();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
function createHttpLogger(logger, options) {
|
|
568
|
+
const {
|
|
569
|
+
reqMapper,
|
|
570
|
+
resMapper,
|
|
571
|
+
level = "INFO",
|
|
572
|
+
skip,
|
|
573
|
+
extractContext
|
|
574
|
+
} = options;
|
|
575
|
+
return (req, res) => {
|
|
576
|
+
if (skip?.(req)) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const startTime = performance.now();
|
|
580
|
+
let finished = false;
|
|
581
|
+
resMapper.onFinish(res, () => {
|
|
582
|
+
if (finished) return;
|
|
583
|
+
finished = true;
|
|
584
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
585
|
+
const statusCode = resMapper.getStatusCode(res);
|
|
586
|
+
const method = reqMapper.getMethod(req);
|
|
587
|
+
const url = reqMapper.getUrl(req);
|
|
588
|
+
const meta = {
|
|
589
|
+
method,
|
|
590
|
+
url,
|
|
591
|
+
statusCode,
|
|
592
|
+
durationMs,
|
|
593
|
+
ip: reqMapper.getIp ? reqMapper.getIp(req) : void 0,
|
|
594
|
+
userAgent: reqMapper.getUserAgent ? reqMapper.getUserAgent(req) : void 0
|
|
595
|
+
};
|
|
596
|
+
if (extractContext) {
|
|
597
|
+
Object.assign(meta, extractContext(req, res));
|
|
598
|
+
}
|
|
599
|
+
let finalLevel = level;
|
|
600
|
+
if (statusCode >= 500) finalLevel = "ERROR";
|
|
601
|
+
else if (statusCode >= 400 && level === "INFO") finalLevel = "WARN";
|
|
602
|
+
const methodKey = finalLevel.toLowerCase();
|
|
603
|
+
logger[methodKey](
|
|
604
|
+
`${method} ${url} - ${statusCode} (${durationMs}ms)`,
|
|
605
|
+
meta
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
436
611
|
// src/middleware/ip.ts
|
|
437
612
|
function ipMiddleware(options = {}) {
|
|
438
613
|
const targetField = options.fieldName ?? "ip";
|
|
@@ -521,6 +696,43 @@ function ocppMiddleware(options = {}) {
|
|
|
521
696
|
};
|
|
522
697
|
}
|
|
523
698
|
|
|
699
|
+
// src/middleware/otel-trace.ts
|
|
700
|
+
function otelTraceMiddleware(options = {}) {
|
|
701
|
+
let traceApi = options.traceApi ?? null;
|
|
702
|
+
let resolved = !!traceApi;
|
|
703
|
+
if (!resolved) {
|
|
704
|
+
try {
|
|
705
|
+
const { createRequire } = __require("module");
|
|
706
|
+
const dynamicRequire = createRequire(__filename);
|
|
707
|
+
const api = dynamicRequire("@opentelemetry/api");
|
|
708
|
+
traceApi = api;
|
|
709
|
+
resolved = true;
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return (entry, next) => {
|
|
714
|
+
if (resolved && traceApi?.trace) {
|
|
715
|
+
try {
|
|
716
|
+
const activeSpan = traceApi.trace.getActiveSpan?.();
|
|
717
|
+
if (activeSpan) {
|
|
718
|
+
const spanContext = activeSpan.spanContext?.();
|
|
719
|
+
if (spanContext) {
|
|
720
|
+
const meta = entry.meta;
|
|
721
|
+
meta.traceId = spanContext.traceId;
|
|
722
|
+
meta.spanId = spanContext.spanId;
|
|
723
|
+
meta.traceFlags = spanContext.traceFlags;
|
|
724
|
+
if (!entry.correlationId) {
|
|
725
|
+
entry.correlationId = spanContext.traceId;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
next(entry);
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
524
736
|
// src/middleware/redaction.ts
|
|
525
737
|
var DEFAULT_REDACT_VALUE = "[REDACTED]";
|
|
526
738
|
function redactionMiddleware(options) {
|
|
@@ -853,34 +1065,60 @@ function getLevelColor(level) {
|
|
|
853
1065
|
|
|
854
1066
|
// src/transports/file.ts
|
|
855
1067
|
import fs from "fs";
|
|
856
|
-
import
|
|
1068
|
+
import path2 from "path";
|
|
857
1069
|
function fileTransport(options) {
|
|
858
|
-
const { dir, level } = options;
|
|
1070
|
+
const { dir, level, maxSize } = options;
|
|
859
1071
|
const filenamePattern = options.filename ?? "app-%DATE%.log";
|
|
860
1072
|
let currentStream = null;
|
|
861
1073
|
let currentPath = "";
|
|
1074
|
+
let currentSize = 0;
|
|
1075
|
+
let rotationIndex = 0;
|
|
1076
|
+
let cachedDate = "";
|
|
1077
|
+
let cacheExpiry = 0;
|
|
1078
|
+
function getCachedDate() {
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
if (now >= cacheExpiry) {
|
|
1081
|
+
cachedDate = new Date(now).toISOString().split("T")[0];
|
|
1082
|
+
cacheExpiry = now + 1e3;
|
|
1083
|
+
}
|
|
1084
|
+
return cachedDate;
|
|
1085
|
+
}
|
|
862
1086
|
try {
|
|
863
1087
|
fs.mkdirSync(dir, { recursive: true });
|
|
864
1088
|
} catch (err) {
|
|
865
1089
|
console.error(`[voltlog] Failed to create log directory: ${dir}`, err);
|
|
866
1090
|
}
|
|
867
|
-
function getPath() {
|
|
868
|
-
const
|
|
869
|
-
const dateStr = now.toISOString().split("T")[0];
|
|
1091
|
+
function getPath(sizeRotation = false) {
|
|
1092
|
+
const dateStr = getCachedDate();
|
|
870
1093
|
const filename = filenamePattern.replace("%DATE%", dateStr);
|
|
871
|
-
|
|
1094
|
+
if (sizeRotation && rotationIndex > 0) {
|
|
1095
|
+
const ext = path2.extname(filename);
|
|
1096
|
+
const base = filename.slice(0, -ext.length || void 0);
|
|
1097
|
+
return path2.join(dir, `${base}.${rotationIndex}${ext}`);
|
|
1098
|
+
}
|
|
1099
|
+
return path2.join(dir, filename);
|
|
1100
|
+
}
|
|
1101
|
+
function openStream(filePath) {
|
|
1102
|
+
if (currentStream) {
|
|
1103
|
+
currentStream.end();
|
|
1104
|
+
}
|
|
1105
|
+
currentPath = filePath;
|
|
1106
|
+
currentSize = 0;
|
|
1107
|
+
currentStream = fs.createWriteStream(filePath, { flags: "a" });
|
|
1108
|
+
try {
|
|
1109
|
+
const stat = fs.statSync(filePath);
|
|
1110
|
+
currentSize = stat.size;
|
|
1111
|
+
} catch {
|
|
1112
|
+
}
|
|
1113
|
+
currentStream.on("error", (err) => {
|
|
1114
|
+
console.error(`[voltlog] File write error to ${filePath}:`, err);
|
|
1115
|
+
});
|
|
872
1116
|
}
|
|
873
1117
|
function rotate() {
|
|
874
1118
|
const newPath = getPath();
|
|
875
1119
|
if (newPath !== currentPath) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
currentPath = newPath;
|
|
880
|
-
currentStream = fs.createWriteStream(newPath, { flags: "a" });
|
|
881
|
-
currentStream.on("error", (err) => {
|
|
882
|
-
console.error(`[voltlog] File write error to ${newPath}:`, err);
|
|
883
|
-
});
|
|
1120
|
+
rotationIndex = 0;
|
|
1121
|
+
openStream(newPath);
|
|
884
1122
|
}
|
|
885
1123
|
}
|
|
886
1124
|
rotate();
|
|
@@ -889,10 +1127,15 @@ function fileTransport(options) {
|
|
|
889
1127
|
level,
|
|
890
1128
|
write(entry) {
|
|
891
1129
|
rotate();
|
|
892
|
-
|
|
893
|
-
const line = `${JSON.stringify(entry)}
|
|
1130
|
+
const line = `${JSON.stringify(entry)}
|
|
894
1131
|
`;
|
|
1132
|
+
if (maxSize && currentSize + line.length > maxSize) {
|
|
1133
|
+
rotationIndex++;
|
|
1134
|
+
openStream(getPath(true));
|
|
1135
|
+
}
|
|
1136
|
+
if (currentStream && !currentStream.writableEnded) {
|
|
895
1137
|
currentStream.write(line);
|
|
1138
|
+
currentSize += line.length;
|
|
896
1139
|
}
|
|
897
1140
|
},
|
|
898
1141
|
async flush() {
|
|
@@ -933,9 +1176,13 @@ function jsonStreamTransport(options) {
|
|
|
933
1176
|
|
|
934
1177
|
// src/transports/loki.ts
|
|
935
1178
|
function lokiTransport(options) {
|
|
936
|
-
const { host,
|
|
1179
|
+
const { host, level } = options;
|
|
1180
|
+
const staticLabels = options.labels ?? { app: "voltlog" };
|
|
937
1181
|
const batchSize = options.batchSize ?? 10;
|
|
938
1182
|
const interval = options.interval ?? 5e3;
|
|
1183
|
+
const includeMetadata = options.includeMetadata !== false;
|
|
1184
|
+
const retryEnabled = options.retry ?? false;
|
|
1185
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
939
1186
|
const url = `${host.replace(/\/$/, "")}/loki/api/v1/push`;
|
|
940
1187
|
const headers = {
|
|
941
1188
|
"Content-Type": "application/json"
|
|
@@ -949,44 +1196,256 @@ function lokiTransport(options) {
|
|
|
949
1196
|
}
|
|
950
1197
|
let buffer = [];
|
|
951
1198
|
let timer = null;
|
|
952
|
-
|
|
1199
|
+
function buildLogLine(entry) {
|
|
1200
|
+
const payload = {
|
|
1201
|
+
level: entry.levelName,
|
|
1202
|
+
message: entry.message,
|
|
1203
|
+
...entry.meta
|
|
1204
|
+
};
|
|
1205
|
+
if (includeMetadata) {
|
|
1206
|
+
if (entry.correlationId) {
|
|
1207
|
+
payload.correlationId = entry.correlationId;
|
|
1208
|
+
}
|
|
1209
|
+
if (entry.context) {
|
|
1210
|
+
payload.context = entry.context;
|
|
1211
|
+
}
|
|
1212
|
+
if (entry.error) {
|
|
1213
|
+
payload.error = entry.error;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return JSON.stringify(payload);
|
|
1217
|
+
}
|
|
1218
|
+
function buildStreams(batch) {
|
|
1219
|
+
if (!options.dynamicLabels) {
|
|
1220
|
+
return [
|
|
1221
|
+
{
|
|
1222
|
+
stream: staticLabels,
|
|
1223
|
+
values: batch.map((e) => [
|
|
1224
|
+
String(e.timestamp * 1e6),
|
|
1225
|
+
// Loki wants nanoseconds
|
|
1226
|
+
buildLogLine(e)
|
|
1227
|
+
])
|
|
1228
|
+
}
|
|
1229
|
+
];
|
|
1230
|
+
}
|
|
1231
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1232
|
+
for (const entry of batch) {
|
|
1233
|
+
const dynamic = options.dynamicLabels(entry);
|
|
1234
|
+
const merged = { ...staticLabels };
|
|
1235
|
+
for (const [k, v] of Object.entries(dynamic)) {
|
|
1236
|
+
if (v !== void 0) merged[k] = v;
|
|
1237
|
+
}
|
|
1238
|
+
const key = JSON.stringify(merged);
|
|
1239
|
+
let group = grouped.get(key);
|
|
1240
|
+
if (!group) {
|
|
1241
|
+
group = { labels: merged, values: [] };
|
|
1242
|
+
grouped.set(key, group);
|
|
1243
|
+
}
|
|
1244
|
+
group.values.push([
|
|
1245
|
+
String(entry.timestamp * 1e6),
|
|
1246
|
+
buildLogLine(entry)
|
|
1247
|
+
]);
|
|
1248
|
+
}
|
|
1249
|
+
return Array.from(grouped.values()).map((g) => ({
|
|
1250
|
+
stream: g.labels,
|
|
1251
|
+
values: g.values
|
|
1252
|
+
}));
|
|
1253
|
+
}
|
|
1254
|
+
async function pushWithRetry(batch) {
|
|
1255
|
+
const body = JSON.stringify({ streams: buildStreams(batch) });
|
|
1256
|
+
let lastError;
|
|
1257
|
+
const attempts = retryEnabled ? maxRetries : 1;
|
|
1258
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1259
|
+
try {
|
|
1260
|
+
const response = await fetch(url, { method: "POST", headers, body });
|
|
1261
|
+
if (response.ok) return;
|
|
1262
|
+
if (response.status >= 400 && response.status < 500) {
|
|
1263
|
+
console.error(`[voltlog] Loki push failed: ${response.status}`);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
lastError = new Error(`Loki HTTP ${response.status}`);
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
lastError = err;
|
|
1269
|
+
}
|
|
1270
|
+
if (attempt < attempts - 1) {
|
|
1271
|
+
await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
console.error("[voltlog] Loki push failed after retries", lastError);
|
|
1275
|
+
}
|
|
1276
|
+
const doFlush = async () => {
|
|
953
1277
|
if (buffer.length === 0) return;
|
|
954
1278
|
const batch = buffer;
|
|
955
1279
|
buffer = [];
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1280
|
+
await pushWithRetry(batch);
|
|
1281
|
+
};
|
|
1282
|
+
const schedule = () => {
|
|
1283
|
+
if (!timer) {
|
|
1284
|
+
timer = setTimeout(() => {
|
|
1285
|
+
timer = null;
|
|
1286
|
+
doFlush();
|
|
1287
|
+
}, interval);
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
return {
|
|
1291
|
+
name: "loki",
|
|
1292
|
+
level,
|
|
1293
|
+
write(entry) {
|
|
1294
|
+
buffer.push(entry);
|
|
1295
|
+
if (buffer.length >= batchSize) {
|
|
1296
|
+
if (timer) clearTimeout(timer);
|
|
1297
|
+
timer = null;
|
|
1298
|
+
doFlush();
|
|
1299
|
+
} else {
|
|
1300
|
+
schedule();
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
async flush() {
|
|
1304
|
+
if (timer) clearTimeout(timer);
|
|
1305
|
+
await doFlush();
|
|
1306
|
+
},
|
|
1307
|
+
async close() {
|
|
1308
|
+
await this.flush?.();
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/transports/otel.ts
|
|
1314
|
+
var OTEL_SEVERITY_MAP = {
|
|
1315
|
+
TRACE: { number: 1, text: "TRACE" },
|
|
1316
|
+
DEBUG: { number: 5, text: "DEBUG" },
|
|
1317
|
+
INFO: { number: 9, text: "INFO" },
|
|
1318
|
+
WARN: { number: 13, text: "WARN" },
|
|
1319
|
+
ERROR: { number: 17, text: "ERROR" },
|
|
1320
|
+
FATAL: { number: 21, text: "FATAL" }
|
|
1321
|
+
};
|
|
1322
|
+
function otelTransport(options) {
|
|
1323
|
+
const { endpoint, serviceName, level, resource = {} } = options;
|
|
1324
|
+
const batchSize = options.batchSize ?? 20;
|
|
1325
|
+
const interval = options.interval ?? 5e3;
|
|
1326
|
+
const url = `${endpoint.replace(/\/$/, "")}/v1/logs`;
|
|
1327
|
+
const headers = {
|
|
1328
|
+
"Content-Type": "application/json",
|
|
1329
|
+
...options.headers
|
|
1330
|
+
};
|
|
1331
|
+
const resourceAttributes = [
|
|
1332
|
+
{ key: "service.name", value: { stringValue: serviceName } },
|
|
1333
|
+
...Object.entries(resource).map(([key, val]) => ({
|
|
1334
|
+
key,
|
|
1335
|
+
value: { stringValue: val }
|
|
1336
|
+
}))
|
|
1337
|
+
];
|
|
1338
|
+
let buffer = [];
|
|
1339
|
+
let timer = null;
|
|
1340
|
+
function toOtlpLogRecord(entry) {
|
|
1341
|
+
const severity = OTEL_SEVERITY_MAP[entry.levelName] ?? OTEL_SEVERITY_MAP.INFO;
|
|
1342
|
+
const attributes = [];
|
|
1343
|
+
if (entry.meta && typeof entry.meta === "object") {
|
|
1344
|
+
for (const [key, val] of Object.entries(entry.meta)) {
|
|
1345
|
+
if (key === "traceId" || key === "spanId" || key === "traceFlags")
|
|
1346
|
+
continue;
|
|
1347
|
+
if (val !== void 0 && val !== null) {
|
|
1348
|
+
attributes.push({
|
|
1349
|
+
key,
|
|
1350
|
+
value: typeof val === "number" ? { intValue: val } : { stringValue: String(val) }
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (entry.context) {
|
|
1356
|
+
for (const [key, val] of Object.entries(entry.context)) {
|
|
1357
|
+
if (val !== void 0 && val !== null) {
|
|
1358
|
+
attributes.push({
|
|
1359
|
+
key: `context.${key}`,
|
|
1360
|
+
value: { stringValue: String(val) }
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (entry.error) {
|
|
1366
|
+
attributes.push({
|
|
1367
|
+
key: "error.message",
|
|
1368
|
+
value: { stringValue: entry.error.message }
|
|
1369
|
+
});
|
|
1370
|
+
if (entry.error.name) {
|
|
1371
|
+
attributes.push({
|
|
1372
|
+
key: "error.type",
|
|
1373
|
+
value: { stringValue: entry.error.name }
|
|
1374
|
+
});
|
|
968
1375
|
}
|
|
969
|
-
|
|
1376
|
+
if (entry.error.stack) {
|
|
1377
|
+
attributes.push({
|
|
1378
|
+
key: "error.stack",
|
|
1379
|
+
value: { stringValue: entry.error.stack }
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const record = {
|
|
1384
|
+
timeUnixNano: String(entry.timestamp * 1e6),
|
|
1385
|
+
// ms → ns
|
|
1386
|
+
severityNumber: severity?.number,
|
|
1387
|
+
severityText: severity?.text,
|
|
1388
|
+
body: { stringValue: entry.message },
|
|
1389
|
+
attributes
|
|
1390
|
+
};
|
|
1391
|
+
const meta = entry.meta;
|
|
1392
|
+
if (meta?.traceId) {
|
|
1393
|
+
record.traceId = meta.traceId;
|
|
1394
|
+
}
|
|
1395
|
+
if (meta?.spanId) {
|
|
1396
|
+
record.spanId = meta.spanId;
|
|
1397
|
+
}
|
|
1398
|
+
if (meta?.traceFlags !== void 0) {
|
|
1399
|
+
record.flags = meta.traceFlags;
|
|
1400
|
+
}
|
|
1401
|
+
return record;
|
|
1402
|
+
}
|
|
1403
|
+
async function sendBatch(batch) {
|
|
1404
|
+
const payload = {
|
|
1405
|
+
resourceLogs: [
|
|
1406
|
+
{
|
|
1407
|
+
resource: { attributes: resourceAttributes },
|
|
1408
|
+
scopeLogs: [
|
|
1409
|
+
{
|
|
1410
|
+
scope: { name: "voltlog-io" },
|
|
1411
|
+
logRecords: batch.map(toOtlpLogRecord)
|
|
1412
|
+
}
|
|
1413
|
+
]
|
|
1414
|
+
}
|
|
1415
|
+
]
|
|
1416
|
+
};
|
|
970
1417
|
try {
|
|
971
|
-
await fetch(url, {
|
|
1418
|
+
const response = await fetch(url, {
|
|
972
1419
|
method: "POST",
|
|
973
1420
|
headers,
|
|
974
|
-
body: JSON.stringify(
|
|
1421
|
+
body: JSON.stringify(payload)
|
|
975
1422
|
});
|
|
1423
|
+
if (!response.ok) {
|
|
1424
|
+
console.error(
|
|
1425
|
+
`[voltlog] OTLP push failed: ${response.status} ${response.statusText}`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
976
1428
|
} catch (err) {
|
|
977
|
-
console.error("[voltlog]
|
|
1429
|
+
console.error("[voltlog] OTLP push failed", err);
|
|
978
1430
|
}
|
|
979
|
-
}
|
|
980
|
-
|
|
1431
|
+
}
|
|
1432
|
+
function flush() {
|
|
1433
|
+
if (buffer.length === 0) return;
|
|
1434
|
+
const batch = buffer;
|
|
1435
|
+
buffer = [];
|
|
1436
|
+
sendBatch(batch).catch(() => {
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
function schedule() {
|
|
981
1440
|
if (!timer) {
|
|
982
1441
|
timer = setTimeout(() => {
|
|
983
1442
|
timer = null;
|
|
984
1443
|
flush();
|
|
985
1444
|
}, interval);
|
|
986
1445
|
}
|
|
987
|
-
}
|
|
1446
|
+
}
|
|
988
1447
|
return {
|
|
989
|
-
name: "
|
|
1448
|
+
name: "otel",
|
|
990
1449
|
level,
|
|
991
1450
|
write(entry) {
|
|
992
1451
|
buffer.push(entry);
|
|
@@ -1000,7 +1459,11 @@ function lokiTransport(options) {
|
|
|
1000
1459
|
},
|
|
1001
1460
|
async flush() {
|
|
1002
1461
|
if (timer) clearTimeout(timer);
|
|
1003
|
-
|
|
1462
|
+
timer = null;
|
|
1463
|
+
if (buffer.length === 0) return;
|
|
1464
|
+
const batch = buffer;
|
|
1465
|
+
buffer = [];
|
|
1466
|
+
await sendBatch(batch);
|
|
1004
1467
|
},
|
|
1005
1468
|
async close() {
|
|
1006
1469
|
await this.flush?.();
|
|
@@ -1046,6 +1509,8 @@ var DIRECTION_ARROWS = {
|
|
|
1046
1509
|
function prettyTransport(options = {}) {
|
|
1047
1510
|
const showTimestamps = options.timestamps ?? true;
|
|
1048
1511
|
const useColors = options.colors ?? true;
|
|
1512
|
+
const hideMeta = options.hideMeta ?? false;
|
|
1513
|
+
const prettyMeta = options.prettyMeta ?? false;
|
|
1049
1514
|
function colorize(text, color) {
|
|
1050
1515
|
return useColors ? `${color}${text}${RESET}` : text;
|
|
1051
1516
|
}
|
|
@@ -1078,10 +1543,27 @@ ${statusIcon} ${status} ${colorize(latency, DIM)}`;
|
|
|
1078
1543
|
const ts = showTimestamps ? `${colorize(new Date(entry.timestamp).toISOString(), DIM)} ` : "";
|
|
1079
1544
|
let line = `${icon} ${ts}${level} ${entry.message}`;
|
|
1080
1545
|
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
1081
|
-
|
|
1546
|
+
const kvStr = Object.entries(entry.context).map(
|
|
1547
|
+
([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`
|
|
1548
|
+
).join(" ");
|
|
1549
|
+
line += ` ${colorize(kvStr, DIM)}`;
|
|
1082
1550
|
}
|
|
1083
|
-
if (entry.meta && Object.keys(entry.meta).length > 0) {
|
|
1084
|
-
|
|
1551
|
+
if (!hideMeta && entry.meta && Object.keys(entry.meta).length > 0) {
|
|
1552
|
+
if (prettyMeta) {
|
|
1553
|
+
const metaEntries = Object.entries(
|
|
1554
|
+
entry.meta
|
|
1555
|
+
);
|
|
1556
|
+
const parts = metaEntries.map(([key, value]) => {
|
|
1557
|
+
const valStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
1558
|
+
return `${colorize(`${key}:`, DIM)}${valStr}`;
|
|
1559
|
+
});
|
|
1560
|
+
line += ` ${parts.join(" ")}`;
|
|
1561
|
+
} else {
|
|
1562
|
+
const kvStr = Object.entries(entry.meta).map(
|
|
1563
|
+
([k, v]) => `${k}:${typeof v === "string" ? v : JSON.stringify(v)}`
|
|
1564
|
+
).join(" ");
|
|
1565
|
+
line += ` ${colorize(kvStr, DIM)}`;
|
|
1566
|
+
}
|
|
1085
1567
|
}
|
|
1086
1568
|
if (entry.error) {
|
|
1087
1569
|
line += `
|
|
@@ -1157,6 +1639,55 @@ function redisTransport(options) {
|
|
|
1157
1639
|
};
|
|
1158
1640
|
}
|
|
1159
1641
|
|
|
1642
|
+
// src/transports/ring-buffer.ts
|
|
1643
|
+
function ringBufferTransport(options = {}) {
|
|
1644
|
+
const maxSize = options.maxSize ?? 1e3;
|
|
1645
|
+
const buffer = [];
|
|
1646
|
+
let head = 0;
|
|
1647
|
+
let count = 0;
|
|
1648
|
+
return {
|
|
1649
|
+
name: "ring-buffer",
|
|
1650
|
+
level: options.level,
|
|
1651
|
+
write(entry) {
|
|
1652
|
+
if (count < maxSize) {
|
|
1653
|
+
buffer.push(entry);
|
|
1654
|
+
count++;
|
|
1655
|
+
} else {
|
|
1656
|
+
buffer[head] = entry;
|
|
1657
|
+
}
|
|
1658
|
+
head = (head + 1) % maxSize;
|
|
1659
|
+
},
|
|
1660
|
+
getEntries(query) {
|
|
1661
|
+
let entries;
|
|
1662
|
+
if (count < maxSize) {
|
|
1663
|
+
entries = buffer.slice();
|
|
1664
|
+
} else {
|
|
1665
|
+
entries = [...buffer.slice(head), ...buffer.slice(0, head)];
|
|
1666
|
+
}
|
|
1667
|
+
if (query?.level) {
|
|
1668
|
+
const minLevel = resolveLevel(query.level);
|
|
1669
|
+
entries = entries.filter((e) => e.level >= minLevel);
|
|
1670
|
+
}
|
|
1671
|
+
if (query?.since) {
|
|
1672
|
+
const since = query.since;
|
|
1673
|
+
entries = entries.filter((e) => e.timestamp >= since);
|
|
1674
|
+
}
|
|
1675
|
+
if (query?.limit) {
|
|
1676
|
+
entries = entries.slice(-query.limit);
|
|
1677
|
+
}
|
|
1678
|
+
return entries;
|
|
1679
|
+
},
|
|
1680
|
+
clear() {
|
|
1681
|
+
buffer.length = 0;
|
|
1682
|
+
head = 0;
|
|
1683
|
+
count = 0;
|
|
1684
|
+
},
|
|
1685
|
+
get size() {
|
|
1686
|
+
return count;
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1160
1691
|
// src/transports/sentry.ts
|
|
1161
1692
|
function sentryTransport(options) {
|
|
1162
1693
|
const { sentry } = options;
|
|
@@ -1390,10 +1921,12 @@ export {
|
|
|
1390
1921
|
LogLevelValueMap,
|
|
1391
1922
|
aiEnrichmentMiddleware,
|
|
1392
1923
|
alertMiddleware,
|
|
1924
|
+
asyncContextMiddleware,
|
|
1393
1925
|
batchTransport,
|
|
1394
1926
|
browserJsonStreamTransport,
|
|
1395
1927
|
consoleTransport,
|
|
1396
1928
|
correlationIdMiddleware,
|
|
1929
|
+
createHttpLogger,
|
|
1397
1930
|
createLogger,
|
|
1398
1931
|
createMiddleware,
|
|
1399
1932
|
createOpenAiErrorAnalyzer,
|
|
@@ -1407,11 +1940,15 @@ export {
|
|
|
1407
1940
|
jsonStreamTransport,
|
|
1408
1941
|
levelOverrideMiddleware,
|
|
1409
1942
|
lokiTransport,
|
|
1943
|
+
nodeHttpMappers,
|
|
1410
1944
|
ocppMiddleware,
|
|
1945
|
+
otelTraceMiddleware,
|
|
1946
|
+
otelTransport,
|
|
1411
1947
|
prettyTransport,
|
|
1412
1948
|
redactionMiddleware,
|
|
1413
1949
|
redisTransport,
|
|
1414
1950
|
resolveLevel,
|
|
1951
|
+
ringBufferTransport,
|
|
1415
1952
|
samplingMiddleware,
|
|
1416
1953
|
sentryTransport,
|
|
1417
1954
|
shouldIncludeStack,
|