logquill 0.3.0 → 1.0.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/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { createHash } from 'crypto';
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import { randomUUID, randomBytes, createHash } from 'crypto';
3
+ import { createRequire } from 'module';
2
4
  import { gzipSync } from 'zlib';
3
5
  import { mkdirSync, openSync, writeSync, fstatSync, closeSync, existsSync, unlinkSync, renameSync } from 'fs';
4
6
  import { dirname } from 'path';
@@ -54,6 +56,7 @@ function createRecord(params) {
54
56
 
55
57
  // src/core/formatter.ts
56
58
  var JSONFormatter = class {
59
+ /** Returns `JSON.stringify(record)`. */
57
60
  format(record) {
58
61
  return JSON.stringify(record);
59
62
  }
@@ -72,6 +75,7 @@ var FunctionPlugin = class {
72
75
 
73
76
  // src/plugins/context-plugin.ts
74
77
  var ContextPlugin = class {
78
+ /** Fixed key/value pairs merged into every record's `meta`. */
75
79
  context;
76
80
  constructor(context) {
77
81
  this.context = context;
@@ -80,11 +84,297 @@ var ContextPlugin = class {
80
84
  return { ...record, meta: { ...this.context, ...record.meta } };
81
85
  }
82
86
  };
87
+ var contextStore = new AsyncLocalStorage();
88
+ function currentContext() {
89
+ return contextStore.getStore() ?? {};
90
+ }
91
+ function bindContext(values, fn) {
92
+ return contextStore.run({ ...currentContext(), ...values }, fn);
93
+ }
94
+
95
+ // src/plugins/rate-limit-plugin.ts
96
+ function defaultKey(record) {
97
+ return `${record.logger}:${record.level}`;
98
+ }
99
+ var RateLimitPlugin = class {
100
+ /** Records allowed per key within a `perSeconds` window before further records for that key are dropped. */
101
+ maxRecords;
102
+ /** Length, in seconds, of each key's rolling window. */
103
+ perSeconds;
104
+ /** Distinct keys tracked at once before the least-recently-seen one is evicted. */
105
+ maxKeys;
106
+ keyFunc;
107
+ clock;
108
+ windows = /* @__PURE__ */ new Map();
109
+ constructor(maxRecords, perSeconds, options = {}) {
110
+ if (maxRecords < 1) {
111
+ throw new Error(`maxRecords must be at least 1, got ${String(maxRecords)}`);
112
+ }
113
+ if (perSeconds <= 0) {
114
+ throw new Error(`perSeconds must be positive, got ${String(perSeconds)}`);
115
+ }
116
+ this.maxRecords = maxRecords;
117
+ this.perSeconds = perSeconds;
118
+ this.keyFunc = options.keyFunc ?? defaultKey;
119
+ this.maxKeys = options.maxKeys ?? 1e3;
120
+ this.clock = options.clock ?? (() => performance.now() / 1e3);
121
+ }
122
+ beforeLog(record) {
123
+ const key = this.keyFunc(record);
124
+ const now = this.clock();
125
+ const window = this.windows.get(key);
126
+ if (window === void 0 || now - window.start >= this.perSeconds) {
127
+ this.windows.delete(key);
128
+ this.windows.set(key, { start: now, count: 1 });
129
+ while (this.windows.size > this.maxKeys) {
130
+ this.evictOldest();
131
+ }
132
+ return record;
133
+ }
134
+ this.windows.delete(key);
135
+ this.windows.set(key, window);
136
+ if (window.count >= this.maxRecords) {
137
+ return null;
138
+ }
139
+ window.count += 1;
140
+ return record;
141
+ }
142
+ evictOldest() {
143
+ const oldest = this.windows.keys().next();
144
+ if (!oldest.done) {
145
+ this.windows.delete(oldest.value);
146
+ }
147
+ }
148
+ };
149
+
150
+ // src/bridges/level-dispatch.ts
151
+ function callAtLevel(log, level, message, meta) {
152
+ switch (level) {
153
+ case 5 /* TRACE */:
154
+ log.trace(message, meta);
155
+ return;
156
+ case 10 /* DEBUG */:
157
+ log.debug(message, meta);
158
+ return;
159
+ case 20 /* INFO */:
160
+ log.info(message, meta);
161
+ return;
162
+ case 30 /* WARN */:
163
+ log.warn(message, meta);
164
+ return;
165
+ case 40 /* ERROR */:
166
+ log.error(message, meta);
167
+ return;
168
+ case 50 /* FATAL */:
169
+ log.fatal(message, meta);
170
+ return;
171
+ }
172
+ }
173
+
174
+ // src/bridges/pino-destination.ts
175
+ var DEFAULT_PINO_LEVEL_MAP = {
176
+ 10: 5 /* TRACE */,
177
+ 20: 10 /* DEBUG */,
178
+ 30: 20 /* INFO */,
179
+ 40: 30 /* WARN */,
180
+ 50: 40 /* ERROR */,
181
+ 60: 50 /* FATAL */
182
+ };
183
+ var LogQuillPinoDestination = class {
184
+ /**
185
+ * Pino only recognizes a bare object passed as its sole argument
186
+ * (`pino(destination)`, without a separate options argument) as a
187
+ * destination stream, rather than an options object, if it looks
188
+ * stream-like — checking for `.writable`/`._writableState`, the same
189
+ * duck-typing `stream.Writable` itself satisfies. Without this, `pino()`
190
+ * would silently fall back to writing to `process.stdout` instead.
191
+ */
192
+ writable = true;
193
+ target;
194
+ levelMap;
195
+ constructor(logquillLogger, options = {}) {
196
+ this.target = logquillLogger;
197
+ this.levelMap = options.levelMap ?? DEFAULT_PINO_LEVEL_MAP;
198
+ }
199
+ /** Node `Writable`-compatible write hook — splits `chunk` into NDJSON lines and dispatches each as a `Logger` call. */
200
+ write(chunk) {
201
+ for (const line of chunk.split("\n")) {
202
+ if (line.trim()) {
203
+ this.writeLine(line);
204
+ }
205
+ }
206
+ return true;
207
+ }
208
+ writeLine(line) {
209
+ let parsed;
210
+ try {
211
+ parsed = JSON.parse(line);
212
+ } catch {
213
+ return;
214
+ }
215
+ if (typeof parsed !== "object" || parsed === null) {
216
+ return;
217
+ }
218
+ const entry = parsed;
219
+ const pinoLevel = typeof entry.level === "number" ? entry.level : 30;
220
+ const message = typeof entry.msg === "string" ? entry.msg : "";
221
+ const meta = {};
222
+ for (const key of Object.keys(entry)) {
223
+ if (key !== "level" && key !== "msg") {
224
+ meta[key] = entry[key];
225
+ }
226
+ }
227
+ callAtLevel(this.target, this.levelMap[pinoLevel] ?? 20 /* INFO */, message, meta);
228
+ }
229
+ };
230
+ var RunPlugin = class {
231
+ /** Id stamped onto `meta.runId` for every record this instance processes. */
232
+ runId;
233
+ step = 0;
234
+ constructor(options = {}) {
235
+ this.runId = options.runId ?? randomUUID();
236
+ }
237
+ beforeLog(record) {
238
+ record.meta.runId ??= this.runId;
239
+ record.meta.step = this.step++;
240
+ return record;
241
+ }
242
+ };
243
+ var traceparentStore = new AsyncLocalStorage();
244
+ function setTraceparent(value) {
245
+ const previous = traceparentStore.getStore();
246
+ traceparentStore.enterWith(value);
247
+ return () => {
248
+ traceparentStore.enterWith(previous);
249
+ };
250
+ }
251
+ function getTraceparent() {
252
+ return traceparentStore.getStore();
253
+ }
254
+ function generateTraceId() {
255
+ return randomBytes(16).toString("hex");
256
+ }
257
+ var W3C_TRACEPARENT_RE = /^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/;
258
+ var XRAY_ROOT_RE = /Root=1-([0-9a-f]{8})-([0-9a-f]{24})/;
259
+ var GCP_TRACE_RE = /^([0-9a-f]{32})\/\d+(;o=\d)?$/;
260
+ function parseTraceHeader(header) {
261
+ const trimmed = header.trim();
262
+ const w3c = W3C_TRACEPARENT_RE.exec(trimmed);
263
+ if (w3c?.[1]) {
264
+ return w3c[1];
265
+ }
266
+ const xray = XRAY_ROOT_RE.exec(trimmed);
267
+ if (xray?.[1] && xray[2]) {
268
+ return xray[1] + xray[2];
269
+ }
270
+ const gcp = GCP_TRACE_RE.exec(trimmed);
271
+ if (gcp?.[1]) {
272
+ return gcp[1];
273
+ }
274
+ return void 0;
275
+ }
276
+ function defaultResolveActiveOtelTraceId() {
277
+ try {
278
+ const require2 = createRequire(import.meta.url);
279
+ const otel = require2("@opentelemetry/api");
280
+ const span = otel.trace.getActiveSpan();
281
+ if (!span) {
282
+ return void 0;
283
+ }
284
+ const spanContext = span.spanContext();
285
+ if (!otel.trace.isSpanContextValid(spanContext)) {
286
+ return void 0;
287
+ }
288
+ return spanContext.traceId;
289
+ } catch {
290
+ return void 0;
291
+ }
292
+ }
293
+ var TraceContextPlugin = class {
294
+ /** `meta` key the trace id is written to. */
295
+ traceKey;
296
+ explicitTraceparent;
297
+ resolveActiveOtelTraceId;
298
+ constructor(options = {}) {
299
+ this.traceKey = options.traceKey ?? "traceId";
300
+ this.explicitTraceparent = options.traceparent;
301
+ this.resolveActiveOtelTraceId = options.resolveActiveOtelTraceId ?? defaultResolveActiveOtelTraceId;
302
+ }
303
+ beforeLog(record) {
304
+ if (record.meta[this.traceKey] != null) {
305
+ return record;
306
+ }
307
+ record.meta[this.traceKey] = this.resolveTraceId();
308
+ return record;
309
+ }
310
+ resolveTraceId() {
311
+ const fromOtel = this.resolveActiveOtelTraceId();
312
+ if (fromOtel) {
313
+ return fromOtel;
314
+ }
315
+ const header = this.explicitTraceparent ?? getTraceparent();
316
+ if (header) {
317
+ const parsed = parseTraceHeader(header);
318
+ if (parsed) {
319
+ return parsed;
320
+ }
321
+ }
322
+ return generateTraceId();
323
+ }
324
+ };
325
+
326
+ // src/plugins/otel-span-processor.ts
327
+ var OTEL_STATUS_CODE_ERROR = 2;
328
+ var OtelSpanProcessor = class {
329
+ log;
330
+ attributesKey;
331
+ constructor(log, options = {}) {
332
+ this.log = log;
333
+ this.attributesKey = options.attributesKey ?? "attributes";
334
+ }
335
+ /** Called by the tracer provider when a span starts — emits `.action()`. */
336
+ onStart(span) {
337
+ this.log.action(span.name, this.baseMeta(span));
338
+ }
339
+ /** Called by the tracer provider when a span ends — emits `.observation()`, or `.error()` if the span's status is an error. */
340
+ onEnd(span) {
341
+ const meta = {
342
+ ...this.baseMeta(span),
343
+ durationMs: span.duration[0] * 1e3 + span.duration[1] / 1e6
344
+ };
345
+ if (span.status.code === OTEL_STATUS_CODE_ERROR) {
346
+ this.log.error(span.name, {
347
+ ...meta,
348
+ error: span.status.message ?? "span ended with an error status"
349
+ });
350
+ return;
351
+ }
352
+ this.log.observation(span.name, meta);
353
+ }
354
+ /** No internal buffering to flush — every span is forwarded to the `Logger` immediately. */
355
+ async forceFlush() {
356
+ }
357
+ /** No resources of its own to release; flush/close the wrapped `Logger` separately. */
358
+ async shutdown() {
359
+ }
360
+ baseMeta(span) {
361
+ const meta = { spanId: span.spanContext().spanId };
362
+ const parentSpanId = span.parentSpanContext?.spanId ?? span.parentSpanId;
363
+ if (parentSpanId) {
364
+ meta.parentSpanId = parentSpanId;
365
+ }
366
+ if (Object.keys(span.attributes).length > 0) {
367
+ meta[this.attributesKey] = span.attributes;
368
+ }
369
+ return meta;
370
+ }
371
+ };
83
372
 
84
373
  // src/plugins/redact-plugin.ts
85
374
  var DEFAULT_REDACTED_KEYS = ["password", "token", "secret", "api_key", "authorization"];
86
375
  var RedactPlugin = class {
87
376
  keys;
377
+ /** Placeholder a matched value is replaced with. */
88
378
  replacement;
89
379
  constructor(options = {}) {
90
380
  this.keys = new Set((options.keys ?? DEFAULT_REDACTED_KEYS).map((key) => key.toLowerCase()));
@@ -108,7 +398,9 @@ var DEFAULT_PII_PATTERNS = {
108
398
  };
109
399
  var MAX_DEPTH = 50;
110
400
  var PIIRedactPlugin = class {
401
+ /** Named patterns scanned for in every string `meta` value. */
111
402
  patterns;
403
+ /** Placeholder a matched substring is replaced with. */
112
404
  replacement;
113
405
  constructor(options = {}) {
114
406
  this.patterns = options.patterns ?? DEFAULT_PII_PATTERNS;
@@ -156,11 +448,17 @@ var PIIRedactPlugin = class {
156
448
 
157
449
  // src/plugins/sampling-plugin.ts
158
450
  var SamplingPlugin = class {
451
+ /** Fraction of non-elevated records kept, in `[0, 1]`. */
159
452
  rate;
453
+ /** `meta` key holding the trace/run id used to group buffered records. */
160
454
  traceKey;
455
+ /** A record at or above this level elevates its whole trace. */
161
456
  elevateAt;
457
+ /** Transports buffered records are flushed straight to on elevation; `undefined` disables tail-based elevation. */
162
458
  transports;
459
+ /** Total buffered records allowed across every trace before the oldest trace is evicted. */
163
460
  maxBufferedRecords;
461
+ /** Distinct trace ids held at once before the oldest is evicted. */
164
462
  maxTraces;
165
463
  rng;
166
464
  buffer = /* @__PURE__ */ new Map();
@@ -306,8 +604,11 @@ function defaultDedupeKey(record) {
306
604
  return `${record.level}:${record.logger}:${record.message}`;
307
605
  }
308
606
  var AlertingPlugin = class {
607
+ /** A record at or above this level fires an alert. */
309
608
  threshold;
609
+ /** How long a dedupe window stays open before a collapsed follow-up alert (if any) fires. */
310
610
  dedupeWindowMs;
611
+ /** Distinct concurrent dedupe keys tracked at once; beyond this, new keys are dropped rather than tracked. */
311
612
  maxTrackedKeys;
312
613
  dedupeKeyFn;
313
614
  windows = /* @__PURE__ */ new Map();
@@ -381,6 +682,7 @@ function formatMessage(record, occurrences) {
381
682
  return `[${record.level}] ${record.logger}: ${record.message}${suffix}`;
382
683
  }
383
684
  var SlackAlertPlugin = class extends AlertingPlugin {
685
+ /** Slack "Incoming Webhook" URL every alert is posted to. */
384
686
  webhookUrl;
385
687
  sender;
386
688
  constructor(webhookUrl, options = {}) {
@@ -410,6 +712,7 @@ async function fetchPagerDutySender(body) {
410
712
  }
411
713
  }
412
714
  var PagerDutyAlertPlugin = class extends AlertingPlugin {
715
+ /** PagerDuty Events API v2 integration key every alert is sent under. */
413
716
  routingKey;
414
717
  sender;
415
718
  constructor(routingKey, options = {}) {
@@ -439,9 +742,13 @@ var PagerDutyAlertPlugin = class extends AlertingPlugin {
439
742
 
440
743
  // src/plugins/email-alert-plugin.ts
441
744
  var EmailAlertPlugin = class extends AlertingPlugin {
745
+ /** SMTP server hostname. */
442
746
  smtpHost;
747
+ /** SMTP server port. */
443
748
  smtpPort;
749
+ /** Envelope `From` address for every alert. */
444
750
  fromAddr;
751
+ /** Envelope `To` addresses for every alert. */
445
752
  toAddrs;
446
753
  username;
447
754
  password;
@@ -507,10 +814,12 @@ var EmailAlertPlugin = class extends AlertingPlugin {
507
814
 
508
815
  // src/transports/transport.ts
509
816
  var Transport = class {
817
+ /** Turns a `LogRecord` into the string this transport actually writes. Defaults to `JSONFormatter`. */
510
818
  formatter;
511
819
  constructor(formatter = new JSONFormatter()) {
512
820
  this.formatter = formatter;
513
821
  }
822
+ /** Formats `record` via `this.formatter`. Called once per record before `write()`. */
514
823
  format(record) {
515
824
  return this.formatter.format(record);
516
825
  }
@@ -518,9 +827,15 @@ var Transport = class {
518
827
  close() {
519
828
  }
520
829
  };
830
+ function hasFlush(transport) {
831
+ return typeof transport.flush === "function";
832
+ }
521
833
  var CollectingTransport = class extends Transport {
834
+ /** Every formatted string passed to `write()`, in call order. */
522
835
  formatted = [];
836
+ /** Every raw `LogRecord` passed to `write()`, in call order. */
523
837
  records = [];
838
+ /** Set once `close()` has been called. */
524
839
  closed = false;
525
840
  write(formatted, record) {
526
841
  this.formatted.push(formatted);
@@ -533,7 +848,9 @@ var CollectingTransport = class extends Transport {
533
848
 
534
849
  // src/transports/batching-transport.ts
535
850
  var BatchingTransport = class extends Transport {
851
+ /** Buffer is flushed once it holds this many items. */
536
852
  maxRecords;
853
+ /** Buffer is flushed once its estimated byte size reaches this many bytes. */
537
854
  maxBytes;
538
855
  buffer = [];
539
856
  bufferBytes = 0;
@@ -550,6 +867,7 @@ var BatchingTransport = class extends Transport {
550
867
  sizeOf(item) {
551
868
  return JSON.stringify(item).length;
552
869
  }
870
+ /** Buffers the record, flushing the batch once `maxRecords`/`maxBytes` is reached. */
553
871
  write(formatted, record) {
554
872
  const item = this.toItem(formatted, record);
555
873
  this.buffer.push(item);
@@ -584,7 +902,9 @@ function metaString(meta, key) {
584
902
  return typeof value === "string" ? value : null;
585
903
  }
586
904
  var BaseSQLTransport = class extends BatchingTransport {
905
+ /** Table records are inserted into. */
587
906
  tableName;
907
+ /** Whether `createTableSQL()` runs before the first insert. Dev/test convenience only. */
588
908
  ensureSchema;
589
909
  schemaEnsured = false;
590
910
  constructor(options = {}) {
@@ -1057,6 +1377,7 @@ var RedisTransport = class extends BatchingTransport {
1057
1377
 
1058
1378
  // src/transports/queue/base-queue-transport.ts
1059
1379
  var BaseQueueTransport = class extends BatchingTransport {
1380
+ /** Destination name on the backend — a Kafka topic, a RabbitMQ queue name, an SQS queue URL, or a GCP Pub/Sub topic. */
1060
1381
  topic;
1061
1382
  constructor(options) {
1062
1383
  super(options);
@@ -1255,8 +1576,11 @@ var PubSubTransport = class extends BaseQueueTransport {
1255
1576
 
1256
1577
  // src/transports/cloud/cloudwatch-transport.ts
1257
1578
  var CloudWatchTransport = class extends BatchingTransport {
1579
+ /** CloudWatch Logs log group written into. */
1258
1580
  logGroupName;
1581
+ /** CloudWatch Logs log stream, within `logGroupName`, written into. */
1259
1582
  logStreamName;
1583
+ /** AWS region passed to the real SDK client; `undefined` when `client` is injected or the SDK's own credential-chain resolution is used. */
1260
1584
  region;
1261
1585
  injectedClient;
1262
1586
  client;
@@ -1324,7 +1648,9 @@ function gcpSeverity(levelValue) {
1324
1648
  }
1325
1649
  }
1326
1650
  var CloudLoggingTransport = class extends BatchingTransport {
1651
+ /** GCP log name (the last segment of the log resource path). */
1327
1652
  logName;
1653
+ /** GCP project ID passed to the real SDK client; `undefined` when `client` is injected or Application Default Credentials' project is used. */
1328
1654
  projectId;
1329
1655
  injectedClient;
1330
1656
  client;
@@ -1389,6 +1715,7 @@ function appInsightsSeverity(levelValue) {
1389
1715
  }
1390
1716
  }
1391
1717
  var AppInsightsTransport = class extends BatchingTransport {
1718
+ /** Azure Application Insights connection string passed to the real SDK client; `undefined` when `client` is injected. */
1392
1719
  connectionString;
1393
1720
  injectedClient;
1394
1721
  client;
@@ -1448,8 +1775,11 @@ async function fetchDatadogSender(url, apiKey, batch) {
1448
1775
  }
1449
1776
  }
1450
1777
  var DatadogTransport = class extends BatchingTransport {
1778
+ /** Logs intake endpoint derived from `site`. */
1451
1779
  url;
1780
+ /** Datadog API key sent in the `DD-API-KEY` header. */
1452
1781
  apiKey;
1782
+ /** Datadog site (region) this transport sends to. */
1453
1783
  site;
1454
1784
  sender;
1455
1785
  constructor(options) {
@@ -1477,7 +1807,9 @@ async function fetchElasticsearchSender(url, headers, body) {
1477
1807
  }
1478
1808
  }
1479
1809
  var ElasticsearchTransport = class extends BatchingTransport {
1810
+ /** `_bulk` endpoint derived from the `node` option. */
1480
1811
  url;
1812
+ /** Index written into. */
1481
1813
  index;
1482
1814
  apiKey;
1483
1815
  sender;
@@ -1528,7 +1860,9 @@ function resumeTimestamp(retryAfter, now) {
1528
1860
  return Number.isNaN(dateMs) ? now + 6e4 : dateMs;
1529
1861
  }
1530
1862
  var NewRelicTransport = class extends BatchingTransport {
1863
+ /** Ingest endpoint derived from `region`. */
1531
1864
  url;
1865
+ /** New Relic account region this transport sends to. */
1532
1866
  region;
1533
1867
  licenseKey;
1534
1868
  sender;
@@ -1595,6 +1929,7 @@ function defaultColorize() {
1595
1929
  return env?.NO_COLOR === void 0;
1596
1930
  }
1597
1931
  var ConsoleTransport = class extends Transport {
1932
+ /** Whether each line is wrapped in an ANSI color escape for its level. */
1598
1933
  colorize;
1599
1934
  out;
1600
1935
  constructor(options = {}) {
@@ -1602,6 +1937,7 @@ var ConsoleTransport = class extends Transport {
1602
1937
  this.colorize = options.colorize ?? defaultColorize();
1603
1938
  this.out = options.console ?? console;
1604
1939
  }
1940
+ /** Writes `formatted` via `console.log`, or `console.error` for ERROR/FATAL records. */
1605
1941
  write(formatted, record) {
1606
1942
  const level = parseLevel(record.level);
1607
1943
  const line = this.colorize ? this.applyColor(formatted, level) : formatted;
@@ -1615,9 +1951,62 @@ var ConsoleTransport = class extends Transport {
1615
1951
  return `${COLORS[level]}${formatted}${RESET}`;
1616
1952
  }
1617
1953
  };
1954
+
1955
+ // src/transports/beacon-transport.ts
1956
+ function defaultBeaconSender(url, batch) {
1957
+ const body = batch.join("\n");
1958
+ const nav = typeof navigator === "undefined" ? void 0 : navigator;
1959
+ if (nav && typeof nav.sendBeacon === "function") {
1960
+ nav.sendBeacon(url, body);
1961
+ return;
1962
+ }
1963
+ void fetch(url, { method: "POST", body, keepalive: true }).catch((error) => {
1964
+ console.error("BeaconTransport: failed to send log batch", error);
1965
+ });
1966
+ }
1967
+ var BeaconTransport = class extends Transport {
1968
+ /** Endpoint each batch is sent to. */
1969
+ url;
1970
+ /** Buffer is flushed once it holds this many lines. */
1971
+ batchSize;
1972
+ sender;
1973
+ batch = [];
1974
+ constructor(url, options = {}) {
1975
+ super(options.formatter);
1976
+ this.url = url;
1977
+ this.batchSize = options.batchSize ?? 20;
1978
+ this.sender = options.sender ?? defaultBeaconSender;
1979
+ }
1980
+ /** Buffers the formatted line, flushing the batch once `batchSize` is reached. */
1981
+ write(formatted) {
1982
+ this.batch.push(formatted);
1983
+ if (this.batch.length >= this.batchSize) {
1984
+ this.flush();
1985
+ }
1986
+ }
1987
+ /** Send the current batch now, even if it hasn't reached `batchSize`. */
1988
+ flush() {
1989
+ if (this.batch.length === 0) {
1990
+ return;
1991
+ }
1992
+ const batch = this.batch;
1993
+ this.batch = [];
1994
+ try {
1995
+ this.sender(this.url, batch);
1996
+ } catch (error) {
1997
+ console.error("BeaconTransport: failed to send log batch", error);
1998
+ }
1999
+ }
2000
+ close() {
2001
+ this.flush();
2002
+ }
2003
+ };
1618
2004
  var FileTransport = class extends Transport {
2005
+ /** Path of the file records are appended to. */
1619
2006
  path;
2007
+ /** File is rotated once it reaches this many bytes. `0` disables rotation. */
1620
2008
  maxBytes;
2009
+ /** How many rotated backups (`.1`, `.2`, ...) are kept. */
1621
2010
  backupCount;
1622
2011
  fd;
1623
2012
  constructor(path, options = {}) {
@@ -1628,6 +2017,7 @@ var FileTransport = class extends Transport {
1628
2017
  mkdirSync(dirname(path), { recursive: true });
1629
2018
  this.fd = openSync(this.path, "a");
1630
2019
  }
2020
+ /** Appends one formatted line to the file, rotating first if `maxBytes` has been exceeded. */
1631
2021
  write(formatted) {
1632
2022
  writeSync(this.fd, formatted + "\n");
1633
2023
  if (this.maxBytes > 0 && fstatSync(this.fd).size >= this.maxBytes) {
@@ -1674,7 +2064,9 @@ async function fetchSender(url, batch) {
1674
2064
  }
1675
2065
  }
1676
2066
  var HTTPTransport = class extends Transport {
2067
+ /** Endpoint each batch is POSTed to. */
1677
2068
  url;
2069
+ /** Buffer is flushed once it holds this many lines. */
1678
2070
  batchSize;
1679
2071
  sender;
1680
2072
  batch = [];
@@ -1684,6 +2076,7 @@ var HTTPTransport = class extends Transport {
1684
2076
  this.batchSize = options.batchSize ?? 50;
1685
2077
  this.sender = options.sender ?? fetchSender;
1686
2078
  }
2079
+ /** Buffers the formatted line, flushing the batch once `batchSize` is reached. */
1687
2080
  write(formatted) {
1688
2081
  this.batch.push(formatted);
1689
2082
  if (this.batch.length >= this.batchSize) {
@@ -1709,13 +2102,182 @@ var HTTPTransport = class extends Transport {
1709
2102
  }
1710
2103
  };
1711
2104
 
2105
+ // src/core/dispatch-queue.ts
2106
+ function isPromise(value) {
2107
+ return typeof value === "object" && typeof value.then === "function";
2108
+ }
2109
+ function defaultScheduler(run) {
2110
+ if (typeof setImmediate === "function") {
2111
+ setImmediate(run);
2112
+ } else {
2113
+ queueMicrotask(run);
2114
+ }
2115
+ }
2116
+ function defaultOnDrop(count, policy) {
2117
+ console.warn(
2118
+ `DispatchQueue: dropped ${String(count)} record(s) \u2014 queue exceeded its configured size under the "${policy}" backpressure policy`
2119
+ );
2120
+ }
2121
+ var DispatchQueue = class {
2122
+ /** Maximum number of pending tasks held at once, as configured via `DispatchQueueOptions`. */
2123
+ maxSize;
2124
+ /** Backpressure policy applied once `maxSize` is reached, as configured via `DispatchQueueOptions`. */
2125
+ policy;
2126
+ onDrop;
2127
+ warnIntervalMs;
2128
+ tasks = [];
2129
+ draining = false;
2130
+ scheduled = false;
2131
+ idleWaiters = [];
2132
+ droppedSinceWarning = 0;
2133
+ lastWarnAt = 0;
2134
+ constructor(options = {}) {
2135
+ this.maxSize = options.maxSize ?? 1e4;
2136
+ this.policy = options.policy ?? "dropOldest";
2137
+ this.onDrop = options.onDrop ?? defaultOnDrop;
2138
+ this.warnIntervalMs = options.warnIntervalMs ?? 5e3;
2139
+ }
2140
+ /** Number of tasks currently waiting to run. Bounded by `maxSize`. */
2141
+ get size() {
2142
+ return this.tasks.length;
2143
+ }
2144
+ /**
2145
+ * Queue `task` to run outside the current call stack, applying the
2146
+ * configured backpressure policy if the queue is already full. Under
2147
+ * `"block"`, `task` may run synchronously before this call returns.
2148
+ */
2149
+ enqueue(task) {
2150
+ if (this.tasks.length < this.maxSize) {
2151
+ this.tasks.push(task);
2152
+ this.schedule();
2153
+ return;
2154
+ }
2155
+ switch (this.policy) {
2156
+ case "dropNewest":
2157
+ this.recordDrop();
2158
+ return;
2159
+ case "block":
2160
+ this.runInline(task);
2161
+ return;
2162
+ case "dropOldest":
2163
+ default:
2164
+ this.tasks.shift();
2165
+ this.recordDrop();
2166
+ this.tasks.push(task);
2167
+ this.schedule();
2168
+ return;
2169
+ }
2170
+ }
2171
+ /** Resolves once every task queued so far has run. Safe to call when idle. */
2172
+ async flush() {
2173
+ if (this.tasks.length === 0 && !this.draining) {
2174
+ return;
2175
+ }
2176
+ await new Promise((resolve) => {
2177
+ this.idleWaiters.push(resolve);
2178
+ this.schedule();
2179
+ });
2180
+ }
2181
+ recordDrop() {
2182
+ this.droppedSinceWarning += 1;
2183
+ const now = Date.now();
2184
+ if (now - this.lastWarnAt >= this.warnIntervalMs) {
2185
+ const count = this.droppedSinceWarning;
2186
+ this.droppedSinceWarning = 0;
2187
+ this.lastWarnAt = now;
2188
+ try {
2189
+ this.onDrop(count, this.policy);
2190
+ } catch {
2191
+ }
2192
+ }
2193
+ }
2194
+ runInline(task) {
2195
+ try {
2196
+ const result = task();
2197
+ if (isPromise(result)) {
2198
+ result.catch((error) => {
2199
+ console.error("DispatchQueue: a blocked task failed", error);
2200
+ });
2201
+ }
2202
+ } catch (error) {
2203
+ console.error("DispatchQueue: a blocked task failed", error);
2204
+ }
2205
+ }
2206
+ schedule() {
2207
+ if (this.scheduled) {
2208
+ return;
2209
+ }
2210
+ this.scheduled = true;
2211
+ defaultScheduler(() => {
2212
+ this.scheduled = false;
2213
+ void this.drainAll();
2214
+ });
2215
+ }
2216
+ async drainAll() {
2217
+ if (this.draining) {
2218
+ return;
2219
+ }
2220
+ this.draining = true;
2221
+ try {
2222
+ while (this.tasks.length > 0) {
2223
+ const task = this.tasks.shift();
2224
+ if (!task) {
2225
+ continue;
2226
+ }
2227
+ try {
2228
+ await task();
2229
+ } catch (error) {
2230
+ console.error("DispatchQueue: a queued task failed", error);
2231
+ }
2232
+ }
2233
+ } finally {
2234
+ this.draining = false;
2235
+ const waiters = this.idleWaiters;
2236
+ this.idleWaiters = [];
2237
+ for (const resolve of waiters) {
2238
+ resolve();
2239
+ }
2240
+ }
2241
+ }
2242
+ };
2243
+ var spanIdStore = new AsyncLocalStorage();
2244
+ function currentSpanId() {
2245
+ return spanIdStore.getStore();
2246
+ }
2247
+ function newSpanId() {
2248
+ return randomBytes(8).toString("hex");
2249
+ }
2250
+ function runInSpan(spanId, fn) {
2251
+ return spanIdStore.run(spanId, fn);
2252
+ }
2253
+
1712
2254
  // src/core/logger.ts
2255
+ function formatSpanError(error) {
2256
+ if (error instanceof Error) {
2257
+ return `${error.name}: ${error.message}`;
2258
+ }
2259
+ return String(error);
2260
+ }
2261
+ function withStackFromErr(meta) {
2262
+ const err = meta.err;
2263
+ if (!(err instanceof Error)) {
2264
+ return meta;
2265
+ }
2266
+ const next = { ...meta };
2267
+ delete next.err;
2268
+ next.stack = err.stack ?? `${err.name}: ${err.message}`;
2269
+ return next;
2270
+ }
1713
2271
  var Logger = class _Logger {
2272
+ /** This logger's name, as passed to the constructor (or derived via `.child()`). Appears on every record as `logger`. */
1714
2273
  name;
2274
+ /** Every transport a written record is sent to. */
1715
2275
  transports;
2276
+ /** Every plugin registered via `.use()`, in registration order. */
1716
2277
  plugins;
1717
2278
  currentLevel;
1718
2279
  baseMeta;
2280
+ dispatchQueue;
1719
2281
  constructor(name, options = {}) {
1720
2282
  this.name = name;
1721
2283
  this.currentLevel = parseLevel(options.level ?? 20 /* INFO */);
@@ -1725,10 +2287,13 @@ var Logger = class _Logger {
1725
2287
  this.use(plugin);
1726
2288
  }
1727
2289
  this.baseMeta = options.meta ? { ...options.meta } : {};
2290
+ this.dispatchQueue = new DispatchQueue(options.queue);
1728
2291
  }
2292
+ /** This logger's current minimum level — records below it are filtered before any plugin runs. */
1729
2293
  get level() {
1730
2294
  return this.currentLevel;
1731
2295
  }
2296
+ /** Changes the minimum level records must meet to reach a transport. Accepts a `Level`, its numeric weight, or its name. */
1732
2297
  setLevel(level) {
1733
2298
  this.currentLevel = parseLevel(level);
1734
2299
  }
@@ -1742,20 +2307,41 @@ var Logger = class _Logger {
1742
2307
  this.plugins.push(typeof plugin === "function" ? new FunctionPlugin(plugin) : plugin);
1743
2308
  return this;
1744
2309
  }
1745
- /** Close every attached transport. Call on shutdown to flush buffered writes. */
1746
- close() {
2310
+ /** Number of dispatched records not yet written to their transports. Bounded by the `queue` option. */
2311
+ get queueSize() {
2312
+ return this.dispatchQueue.size;
2313
+ }
2314
+ /**
2315
+ * Waits for every record dispatched so far to reach its transports'
2316
+ * `write()` (and any plugin `afterLog` hooks). Note this does *not* force
2317
+ * a batching transport (SQL, a queue, `HTTPTransport`, ...) to send a
2318
+ * batch still under its own `maxRecords`/`maxBytes` threshold early — it
2319
+ * only guarantees the record has been handed to that transport, the same
2320
+ * contract `write()` always had. Before a process may pause or exit
2321
+ * (a serverless freeze, a shutdown signal), prefer `withLambda`/
2322
+ * `installShutdownHandlers`, which additionally force every batching
2323
+ * transport to send its current buffer regardless of threshold.
2324
+ */
2325
+ async flush() {
2326
+ await this.dispatchQueue.flush();
2327
+ }
2328
+ /** Flush every pending record, then close every attached transport. Call once, on shutdown. */
2329
+ async close() {
2330
+ await this.flush();
1747
2331
  for (const transport of this.transports) {
1748
2332
  transport.close();
1749
2333
  }
1750
2334
  }
1751
- /** A logger scoped under this one, inheriting its level, transports, and plugins. */
2335
+ /** A logger scoped under this one, inheriting its level, transports, plugins, and dispatch queue. */
1752
2336
  child(name, meta = {}) {
1753
- return new _Logger(`${this.name}.${name}`, {
2337
+ const child = new _Logger(`${this.name}.${name}`, {
1754
2338
  level: this.currentLevel,
1755
2339
  transports: this.transports,
1756
2340
  plugins: this.plugins,
1757
2341
  meta: { ...this.baseMeta, ...meta }
1758
2342
  });
2343
+ child.dispatchQueue = this.dispatchQueue;
2344
+ return child;
1759
2345
  }
1760
2346
  notifyError(plugin, error, record) {
1761
2347
  try {
@@ -1771,8 +2357,12 @@ var Logger = class _Logger {
1771
2357
  level,
1772
2358
  logger: this.name,
1773
2359
  message,
1774
- meta: { ...this.baseMeta, ...meta }
2360
+ meta: { ...this.baseMeta, ...currentContext(), ...withStackFromErr(meta) }
1775
2361
  });
2362
+ const parentSpanId = currentSpanId();
2363
+ if (parentSpanId !== void 0) {
2364
+ record.meta.parentSpanId ??= parentSpanId;
2365
+ }
1776
2366
  for (const plugin of this.plugins) {
1777
2367
  let result;
1778
2368
  try {
@@ -1786,8 +2376,18 @@ var Logger = class _Logger {
1786
2376
  }
1787
2377
  record = result;
1788
2378
  }
2379
+ this.dispatchQueue.enqueue(() => {
2380
+ this.writeAndNotify(record);
2381
+ });
2382
+ return record;
2383
+ }
2384
+ writeAndNotify(record) {
1789
2385
  for (const transport of this.transports) {
1790
- transport.write(transport.format(record), record);
2386
+ try {
2387
+ transport.write(transport.format(record), record);
2388
+ } catch (error) {
2389
+ console.error(`${transport.constructor.name}: failed to write a log record`, error);
2390
+ }
1791
2391
  }
1792
2392
  for (const plugin of this.plugins) {
1793
2393
  try {
@@ -1796,31 +2396,158 @@ var Logger = class _Logger {
1796
2396
  this.notifyError(plugin, error, record);
1797
2397
  }
1798
2398
  }
1799
- return record;
1800
2399
  }
2400
+ /** Logs at `TRACE` — the lowest level, for fine-grained diagnostic detail. Returns the record, or `null` if filtered/dropped. */
1801
2401
  trace(message, meta = {}) {
1802
2402
  return this.dispatch(5 /* TRACE */, message, meta);
1803
2403
  }
2404
+ /** Logs at `DEBUG`. Returns the record, or `null` if filtered/dropped. */
1804
2405
  debug(message, meta = {}) {
1805
2406
  return this.dispatch(10 /* DEBUG */, message, meta);
1806
2407
  }
2408
+ /** Logs at `INFO`. Returns the record, or `null` if filtered/dropped. */
1807
2409
  info(message, meta = {}) {
1808
2410
  return this.dispatch(20 /* INFO */, message, meta);
1809
2411
  }
2412
+ /** Logs at `WARN`. Returns the record, or `null` if filtered/dropped. */
1810
2413
  warn(message, meta = {}) {
1811
2414
  return this.dispatch(30 /* WARN */, message, meta);
1812
2415
  }
2416
+ /** Logs at `ERROR`. Returns the record, or `null` if filtered/dropped. */
1813
2417
  error(message, meta = {}) {
1814
2418
  return this.dispatch(40 /* ERROR */, message, meta);
1815
2419
  }
2420
+ /** Logs at `FATAL` — the highest level, for errors that precede an unrecoverable failure. Returns the record, or `null` if filtered/dropped. */
1816
2421
  fatal(message, meta = {}) {
1817
2422
  return this.dispatch(50 /* FATAL */, message, meta);
1818
2423
  }
2424
+ /** `.info()` tagged `meta.kind = "thought"` — an agent's internal reasoning step, for harness/agentic tracing. */
2425
+ thought(message, meta = {}) {
2426
+ return this.dispatch(20 /* INFO */, message, { kind: "thought", ...meta });
2427
+ }
2428
+ /** `.info()` tagged `meta.kind = "action"` — an agent taking an action (a tool call, an LLM request), for harness/agentic tracing. */
2429
+ action(message, meta = {}) {
2430
+ return this.dispatch(20 /* INFO */, message, { kind: "action", ...meta });
2431
+ }
2432
+ /** `.info()` tagged `meta.kind = "observation"` — the result an agent observed from an action, for harness/agentic tracing. */
2433
+ observation(message, meta = {}) {
2434
+ return this.dispatch(20 /* INFO */, message, { kind: "observation", ...meta });
2435
+ }
2436
+ /** `.info()` tagged `meta.kind = "decision"` — an agent's concluding decision for a step or run, for harness/agentic tracing. */
2437
+ decision(message, meta = {}) {
2438
+ return this.dispatch(20 /* INFO */, message, { kind: "decision", ...meta });
2439
+ }
2440
+ /**
2441
+ * `await logger.span("callLlm", async () => {...})` — runs `fn`, and on
2442
+ * settling (success or throw) emits one record for the span itself
2443
+ * carrying `meta.spanId` and `meta.durationMs`. Every record logged
2444
+ * inside `fn` — through any method, and through any further `await` —
2445
+ * is automatically stamped with `meta.parentSpanId` pointing at this
2446
+ * span, so nested/sub-agent calls reconstruct their exact nesting when
2447
+ * sorted by `spanId`/`parentSpanId`.
2448
+ *
2449
+ * Still emits its record — at `ERROR`, with `meta.error` set — if `fn`
2450
+ * throws; the error itself propagates unchanged to the caller.
2451
+ *
2452
+ * `spanId`/`parentSpanId` normally auto-generate/auto-nest; pass them in
2453
+ * `options` to adopt an id handed in from elsewhere (e.g. a framework
2454
+ * adapter translating an id it already received).
2455
+ */
2456
+ async span(name, fn, options = {}) {
2457
+ const { spanId: explicitSpanId, parentSpanId: explicitParentSpanId, ...meta } = options;
2458
+ const spanId = explicitSpanId ?? newSpanId();
2459
+ const start = performance.now();
2460
+ try {
2461
+ const result = await runInSpan(spanId, () => fn());
2462
+ this.finishSpan(name, spanId, explicitParentSpanId, performance.now() - start, meta);
2463
+ return result;
2464
+ } catch (error) {
2465
+ this.finishSpan(name, spanId, explicitParentSpanId, performance.now() - start, meta, error);
2466
+ throw error;
2467
+ }
2468
+ }
2469
+ finishSpan(name, spanId, explicitParentSpanId, durationMs, meta, error) {
2470
+ const fullMeta = {
2471
+ spanId,
2472
+ durationMs: Math.round(durationMs * 1e3) / 1e3,
2473
+ ...meta
2474
+ };
2475
+ if (explicitParentSpanId !== void 0) {
2476
+ fullMeta.parentSpanId = explicitParentSpanId;
2477
+ }
2478
+ fullMeta.kind ??= "span";
2479
+ if (error !== void 0) {
2480
+ fullMeta.error = formatSpanError(error);
2481
+ }
2482
+ this.dispatch(error !== void 0 ? 40 /* ERROR */ : 20 /* INFO */, name, fullMeta);
2483
+ }
1819
2484
  };
1820
2485
 
2486
+ // src/core/serverless.ts
2487
+ async function flushEverything(logger) {
2488
+ await logger.flush();
2489
+ for (const transport of logger.transports) {
2490
+ if (!hasFlush(transport)) {
2491
+ continue;
2492
+ }
2493
+ try {
2494
+ await transport.flush();
2495
+ } catch (error) {
2496
+ console.error(`${transport.constructor.name}: failed to flush`, error);
2497
+ }
2498
+ }
2499
+ }
2500
+ function withFlush(logger, fn) {
2501
+ return async (...args) => {
2502
+ try {
2503
+ return await fn(...args);
2504
+ } finally {
2505
+ await flushEverything(logger);
2506
+ }
2507
+ };
2508
+ }
2509
+ var withLambda = withFlush;
2510
+ var withCloudFunction = withFlush;
2511
+ var withAzureFunction = withFlush;
2512
+
2513
+ // src/core/shutdown.ts
2514
+ function installShutdownHandlers(logger, options = {}) {
2515
+ const signals = options.signals ?? ["SIGTERM", "SIGINT"];
2516
+ let closed = false;
2517
+ const shutdown = (exitAfter) => {
2518
+ if (closed) {
2519
+ return;
2520
+ }
2521
+ closed = true;
2522
+ logger.close().catch((error) => {
2523
+ console.error("installShutdownHandlers: logger.close() failed during shutdown", error);
2524
+ }).finally(() => {
2525
+ if (exitAfter) {
2526
+ process.exit(0);
2527
+ }
2528
+ });
2529
+ };
2530
+ const onBeforeExit = () => {
2531
+ shutdown(false);
2532
+ };
2533
+ const onSignal = () => {
2534
+ shutdown(true);
2535
+ };
2536
+ process.once("beforeExit", onBeforeExit);
2537
+ for (const signal of signals) {
2538
+ process.once(signal, onSignal);
2539
+ }
2540
+ return () => {
2541
+ process.removeListener("beforeExit", onBeforeExit);
2542
+ for (const signal of signals) {
2543
+ process.removeListener(signal, onSignal);
2544
+ }
2545
+ };
2546
+ }
2547
+
1821
2548
  // src/index.ts
1822
2549
  var VERSION = "0.3.0";
1823
2550
 
1824
- export { AlertingPlugin, AppInsightsTransport, BaseQueueTransport, BaseSQLTransport, BatchingTransport, CloudLoggingTransport, CloudWatchTransport, CollectingTransport, ConsoleTransport, ContextPlugin, DEFAULT_PII_PATTERNS, DEFAULT_REDACTED_KEYS, DatadogTransport, DynamoDBTransport, ElasticsearchTransport, EmailAlertPlugin, FileTransport, FunctionPlugin, GENESIS_HASH, HTTPTransport, JSONFormatter, KafkaTransport, Level, Logger, MongoDBTransport, MySQLTransport, NewRelicTransport, PIIRedactPlugin, PagerDutyAlertPlugin, PostgresTransport, PubSubTransport, RabbitMQTransport, RedactPlugin, RedisTransport, SQLiteTransport, SQSTransport, SamplingPlugin, SlackAlertPlugin, TamperEvidentPlugin, Transport, VERSION, createRecord, levelName, parseLevel, utcTimestamp };
2551
+ export { AlertingPlugin, AppInsightsTransport, BaseQueueTransport, BaseSQLTransport, BatchingTransport, BeaconTransport, CloudLoggingTransport, CloudWatchTransport, CollectingTransport, ConsoleTransport, ContextPlugin, DEFAULT_PII_PATTERNS, DEFAULT_PINO_LEVEL_MAP, DEFAULT_REDACTED_KEYS, DatadogTransport, DispatchQueue, DynamoDBTransport, ElasticsearchTransport, EmailAlertPlugin, FileTransport, FunctionPlugin, GENESIS_HASH, HTTPTransport, JSONFormatter, KafkaTransport, Level, LogQuillPinoDestination, Logger, MongoDBTransport, MySQLTransport, NewRelicTransport, OtelSpanProcessor, PIIRedactPlugin, PagerDutyAlertPlugin, PostgresTransport, PubSubTransport, RabbitMQTransport, RateLimitPlugin, RedactPlugin, RedisTransport, RunPlugin, SQLiteTransport, SQSTransport, SamplingPlugin, SlackAlertPlugin, TamperEvidentPlugin, TraceContextPlugin, Transport, VERSION, bindContext, createRecord, currentContext, defaultResolveActiveOtelTraceId, generateTraceId, getTraceparent, hasFlush, installShutdownHandlers, levelName, parseLevel, parseTraceHeader, setTraceparent, utcTimestamp, withAzureFunction, withCloudFunction, withFlush, withLambda };
1825
2552
  //# sourceMappingURL=index.mjs.map
1826
2553
  //# sourceMappingURL=index.mjs.map