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/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 { createId } from "@paralleldrive/cuid2";
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: createId(),
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
- const logError = {
152
- message: error.message,
153
- name: error.name,
154
- code: error.code
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 { createId as createId2 } from "@paralleldrive/cuid2";
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 ?? createId2;
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 path from "path";
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 now = /* @__PURE__ */ new Date();
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
- return path.join(dir, filename);
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
- if (currentStream) {
877
- currentStream.end();
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
- if (currentStream && !currentStream.writableEnded) {
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, labels = { app: "voltlog" }, level } = options;
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
- const flush = async () => {
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
- const streams = [
957
- {
958
- stream: labels,
959
- values: batch.map((e) => [
960
- String(e.timestamp * 1e6),
961
- // Loki wants nanoseconds
962
- JSON.stringify({
963
- level: e.levelName,
964
- message: e.message,
965
- ...e.meta
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({ streams })
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] Loki push failed", err);
1429
+ console.error("[voltlog] OTLP push failed", err);
978
1430
  }
979
- };
980
- const schedule = () => {
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: "loki",
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
- await flush();
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
- line += ` ${colorize(JSON.stringify(entry.context), DIM)}`;
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
- line += ` ${colorize(JSON.stringify(entry.meta), DIM)}`;
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,