logquill 0.4.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,5 +1,5 @@
1
- import { randomUUID, randomBytes, createHash } from 'crypto';
2
1
  import { AsyncLocalStorage } from 'async_hooks';
2
+ import { randomUUID, randomBytes, createHash } from 'crypto';
3
3
  import { createRequire } from 'module';
4
4
  import { gzipSync } from 'zlib';
5
5
  import { mkdirSync, openSync, writeSync, fstatSync, closeSync, existsSync, unlinkSync, renameSync } from 'fs';
@@ -56,6 +56,7 @@ function createRecord(params) {
56
56
 
57
57
  // src/core/formatter.ts
58
58
  var JSONFormatter = class {
59
+ /** Returns `JSON.stringify(record)`. */
59
60
  format(record) {
60
61
  return JSON.stringify(record);
61
62
  }
@@ -74,6 +75,7 @@ var FunctionPlugin = class {
74
75
 
75
76
  // src/plugins/context-plugin.ts
76
77
  var ContextPlugin = class {
78
+ /** Fixed key/value pairs merged into every record's `meta`. */
77
79
  context;
78
80
  constructor(context) {
79
81
  this.context = context;
@@ -82,7 +84,151 @@ var ContextPlugin = class {
82
84
  return { ...record, meta: { ...this.context, ...record.meta } };
83
85
  }
84
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
+ };
85
230
  var RunPlugin = class {
231
+ /** Id stamped onto `meta.runId` for every record this instance processes. */
86
232
  runId;
87
233
  step = 0;
88
234
  constructor(options = {}) {
@@ -145,6 +291,7 @@ function defaultResolveActiveOtelTraceId() {
145
291
  }
146
292
  }
147
293
  var TraceContextPlugin = class {
294
+ /** `meta` key the trace id is written to. */
148
295
  traceKey;
149
296
  explicitTraceparent;
150
297
  resolveActiveOtelTraceId;
@@ -176,10 +323,58 @@ var TraceContextPlugin = class {
176
323
  }
177
324
  };
178
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
+ };
372
+
179
373
  // src/plugins/redact-plugin.ts
180
374
  var DEFAULT_REDACTED_KEYS = ["password", "token", "secret", "api_key", "authorization"];
181
375
  var RedactPlugin = class {
182
376
  keys;
377
+ /** Placeholder a matched value is replaced with. */
183
378
  replacement;
184
379
  constructor(options = {}) {
185
380
  this.keys = new Set((options.keys ?? DEFAULT_REDACTED_KEYS).map((key) => key.toLowerCase()));
@@ -203,7 +398,9 @@ var DEFAULT_PII_PATTERNS = {
203
398
  };
204
399
  var MAX_DEPTH = 50;
205
400
  var PIIRedactPlugin = class {
401
+ /** Named patterns scanned for in every string `meta` value. */
206
402
  patterns;
403
+ /** Placeholder a matched substring is replaced with. */
207
404
  replacement;
208
405
  constructor(options = {}) {
209
406
  this.patterns = options.patterns ?? DEFAULT_PII_PATTERNS;
@@ -251,11 +448,17 @@ var PIIRedactPlugin = class {
251
448
 
252
449
  // src/plugins/sampling-plugin.ts
253
450
  var SamplingPlugin = class {
451
+ /** Fraction of non-elevated records kept, in `[0, 1]`. */
254
452
  rate;
453
+ /** `meta` key holding the trace/run id used to group buffered records. */
255
454
  traceKey;
455
+ /** A record at or above this level elevates its whole trace. */
256
456
  elevateAt;
457
+ /** Transports buffered records are flushed straight to on elevation; `undefined` disables tail-based elevation. */
257
458
  transports;
459
+ /** Total buffered records allowed across every trace before the oldest trace is evicted. */
258
460
  maxBufferedRecords;
461
+ /** Distinct trace ids held at once before the oldest is evicted. */
259
462
  maxTraces;
260
463
  rng;
261
464
  buffer = /* @__PURE__ */ new Map();
@@ -401,8 +604,11 @@ function defaultDedupeKey(record) {
401
604
  return `${record.level}:${record.logger}:${record.message}`;
402
605
  }
403
606
  var AlertingPlugin = class {
607
+ /** A record at or above this level fires an alert. */
404
608
  threshold;
609
+ /** How long a dedupe window stays open before a collapsed follow-up alert (if any) fires. */
405
610
  dedupeWindowMs;
611
+ /** Distinct concurrent dedupe keys tracked at once; beyond this, new keys are dropped rather than tracked. */
406
612
  maxTrackedKeys;
407
613
  dedupeKeyFn;
408
614
  windows = /* @__PURE__ */ new Map();
@@ -476,6 +682,7 @@ function formatMessage(record, occurrences) {
476
682
  return `[${record.level}] ${record.logger}: ${record.message}${suffix}`;
477
683
  }
478
684
  var SlackAlertPlugin = class extends AlertingPlugin {
685
+ /** Slack "Incoming Webhook" URL every alert is posted to. */
479
686
  webhookUrl;
480
687
  sender;
481
688
  constructor(webhookUrl, options = {}) {
@@ -505,6 +712,7 @@ async function fetchPagerDutySender(body) {
505
712
  }
506
713
  }
507
714
  var PagerDutyAlertPlugin = class extends AlertingPlugin {
715
+ /** PagerDuty Events API v2 integration key every alert is sent under. */
508
716
  routingKey;
509
717
  sender;
510
718
  constructor(routingKey, options = {}) {
@@ -534,9 +742,13 @@ var PagerDutyAlertPlugin = class extends AlertingPlugin {
534
742
 
535
743
  // src/plugins/email-alert-plugin.ts
536
744
  var EmailAlertPlugin = class extends AlertingPlugin {
745
+ /** SMTP server hostname. */
537
746
  smtpHost;
747
+ /** SMTP server port. */
538
748
  smtpPort;
749
+ /** Envelope `From` address for every alert. */
539
750
  fromAddr;
751
+ /** Envelope `To` addresses for every alert. */
540
752
  toAddrs;
541
753
  username;
542
754
  password;
@@ -602,10 +814,12 @@ var EmailAlertPlugin = class extends AlertingPlugin {
602
814
 
603
815
  // src/transports/transport.ts
604
816
  var Transport = class {
817
+ /** Turns a `LogRecord` into the string this transport actually writes. Defaults to `JSONFormatter`. */
605
818
  formatter;
606
819
  constructor(formatter = new JSONFormatter()) {
607
820
  this.formatter = formatter;
608
821
  }
822
+ /** Formats `record` via `this.formatter`. Called once per record before `write()`. */
609
823
  format(record) {
610
824
  return this.formatter.format(record);
611
825
  }
@@ -613,9 +827,15 @@ var Transport = class {
613
827
  close() {
614
828
  }
615
829
  };
830
+ function hasFlush(transport) {
831
+ return typeof transport.flush === "function";
832
+ }
616
833
  var CollectingTransport = class extends Transport {
834
+ /** Every formatted string passed to `write()`, in call order. */
617
835
  formatted = [];
836
+ /** Every raw `LogRecord` passed to `write()`, in call order. */
618
837
  records = [];
838
+ /** Set once `close()` has been called. */
619
839
  closed = false;
620
840
  write(formatted, record) {
621
841
  this.formatted.push(formatted);
@@ -628,7 +848,9 @@ var CollectingTransport = class extends Transport {
628
848
 
629
849
  // src/transports/batching-transport.ts
630
850
  var BatchingTransport = class extends Transport {
851
+ /** Buffer is flushed once it holds this many items. */
631
852
  maxRecords;
853
+ /** Buffer is flushed once its estimated byte size reaches this many bytes. */
632
854
  maxBytes;
633
855
  buffer = [];
634
856
  bufferBytes = 0;
@@ -645,6 +867,7 @@ var BatchingTransport = class extends Transport {
645
867
  sizeOf(item) {
646
868
  return JSON.stringify(item).length;
647
869
  }
870
+ /** Buffers the record, flushing the batch once `maxRecords`/`maxBytes` is reached. */
648
871
  write(formatted, record) {
649
872
  const item = this.toItem(formatted, record);
650
873
  this.buffer.push(item);
@@ -679,7 +902,9 @@ function metaString(meta, key) {
679
902
  return typeof value === "string" ? value : null;
680
903
  }
681
904
  var BaseSQLTransport = class extends BatchingTransport {
905
+ /** Table records are inserted into. */
682
906
  tableName;
907
+ /** Whether `createTableSQL()` runs before the first insert. Dev/test convenience only. */
683
908
  ensureSchema;
684
909
  schemaEnsured = false;
685
910
  constructor(options = {}) {
@@ -1152,6 +1377,7 @@ var RedisTransport = class extends BatchingTransport {
1152
1377
 
1153
1378
  // src/transports/queue/base-queue-transport.ts
1154
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. */
1155
1381
  topic;
1156
1382
  constructor(options) {
1157
1383
  super(options);
@@ -1350,8 +1576,11 @@ var PubSubTransport = class extends BaseQueueTransport {
1350
1576
 
1351
1577
  // src/transports/cloud/cloudwatch-transport.ts
1352
1578
  var CloudWatchTransport = class extends BatchingTransport {
1579
+ /** CloudWatch Logs log group written into. */
1353
1580
  logGroupName;
1581
+ /** CloudWatch Logs log stream, within `logGroupName`, written into. */
1354
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. */
1355
1584
  region;
1356
1585
  injectedClient;
1357
1586
  client;
@@ -1419,7 +1648,9 @@ function gcpSeverity(levelValue) {
1419
1648
  }
1420
1649
  }
1421
1650
  var CloudLoggingTransport = class extends BatchingTransport {
1651
+ /** GCP log name (the last segment of the log resource path). */
1422
1652
  logName;
1653
+ /** GCP project ID passed to the real SDK client; `undefined` when `client` is injected or Application Default Credentials' project is used. */
1423
1654
  projectId;
1424
1655
  injectedClient;
1425
1656
  client;
@@ -1484,6 +1715,7 @@ function appInsightsSeverity(levelValue) {
1484
1715
  }
1485
1716
  }
1486
1717
  var AppInsightsTransport = class extends BatchingTransport {
1718
+ /** Azure Application Insights connection string passed to the real SDK client; `undefined` when `client` is injected. */
1487
1719
  connectionString;
1488
1720
  injectedClient;
1489
1721
  client;
@@ -1543,8 +1775,11 @@ async function fetchDatadogSender(url, apiKey, batch) {
1543
1775
  }
1544
1776
  }
1545
1777
  var DatadogTransport = class extends BatchingTransport {
1778
+ /** Logs intake endpoint derived from `site`. */
1546
1779
  url;
1780
+ /** Datadog API key sent in the `DD-API-KEY` header. */
1547
1781
  apiKey;
1782
+ /** Datadog site (region) this transport sends to. */
1548
1783
  site;
1549
1784
  sender;
1550
1785
  constructor(options) {
@@ -1572,7 +1807,9 @@ async function fetchElasticsearchSender(url, headers, body) {
1572
1807
  }
1573
1808
  }
1574
1809
  var ElasticsearchTransport = class extends BatchingTransport {
1810
+ /** `_bulk` endpoint derived from the `node` option. */
1575
1811
  url;
1812
+ /** Index written into. */
1576
1813
  index;
1577
1814
  apiKey;
1578
1815
  sender;
@@ -1623,7 +1860,9 @@ function resumeTimestamp(retryAfter, now) {
1623
1860
  return Number.isNaN(dateMs) ? now + 6e4 : dateMs;
1624
1861
  }
1625
1862
  var NewRelicTransport = class extends BatchingTransport {
1863
+ /** Ingest endpoint derived from `region`. */
1626
1864
  url;
1865
+ /** New Relic account region this transport sends to. */
1627
1866
  region;
1628
1867
  licenseKey;
1629
1868
  sender;
@@ -1690,6 +1929,7 @@ function defaultColorize() {
1690
1929
  return env?.NO_COLOR === void 0;
1691
1930
  }
1692
1931
  var ConsoleTransport = class extends Transport {
1932
+ /** Whether each line is wrapped in an ANSI color escape for its level. */
1693
1933
  colorize;
1694
1934
  out;
1695
1935
  constructor(options = {}) {
@@ -1697,6 +1937,7 @@ var ConsoleTransport = class extends Transport {
1697
1937
  this.colorize = options.colorize ?? defaultColorize();
1698
1938
  this.out = options.console ?? console;
1699
1939
  }
1940
+ /** Writes `formatted` via `console.log`, or `console.error` for ERROR/FATAL records. */
1700
1941
  write(formatted, record) {
1701
1942
  const level = parseLevel(record.level);
1702
1943
  const line = this.colorize ? this.applyColor(formatted, level) : formatted;
@@ -1710,9 +1951,62 @@ var ConsoleTransport = class extends Transport {
1710
1951
  return `${COLORS[level]}${formatted}${RESET}`;
1711
1952
  }
1712
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
+ };
1713
2004
  var FileTransport = class extends Transport {
2005
+ /** Path of the file records are appended to. */
1714
2006
  path;
2007
+ /** File is rotated once it reaches this many bytes. `0` disables rotation. */
1715
2008
  maxBytes;
2009
+ /** How many rotated backups (`.1`, `.2`, ...) are kept. */
1716
2010
  backupCount;
1717
2011
  fd;
1718
2012
  constructor(path, options = {}) {
@@ -1723,6 +2017,7 @@ var FileTransport = class extends Transport {
1723
2017
  mkdirSync(dirname(path), { recursive: true });
1724
2018
  this.fd = openSync(this.path, "a");
1725
2019
  }
2020
+ /** Appends one formatted line to the file, rotating first if `maxBytes` has been exceeded. */
1726
2021
  write(formatted) {
1727
2022
  writeSync(this.fd, formatted + "\n");
1728
2023
  if (this.maxBytes > 0 && fstatSync(this.fd).size >= this.maxBytes) {
@@ -1769,7 +2064,9 @@ async function fetchSender(url, batch) {
1769
2064
  }
1770
2065
  }
1771
2066
  var HTTPTransport = class extends Transport {
2067
+ /** Endpoint each batch is POSTed to. */
1772
2068
  url;
2069
+ /** Buffer is flushed once it holds this many lines. */
1773
2070
  batchSize;
1774
2071
  sender;
1775
2072
  batch = [];
@@ -1779,6 +2076,7 @@ var HTTPTransport = class extends Transport {
1779
2076
  this.batchSize = options.batchSize ?? 50;
1780
2077
  this.sender = options.sender ?? fetchSender;
1781
2078
  }
2079
+ /** Buffers the formatted line, flushing the batch once `batchSize` is reached. */
1782
2080
  write(formatted) {
1783
2081
  this.batch.push(formatted);
1784
2082
  if (this.batch.length >= this.batchSize) {
@@ -1803,6 +2101,145 @@ var HTTPTransport = class extends Transport {
1803
2101
  this.flush();
1804
2102
  }
1805
2103
  };
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
+ };
1806
2243
  var spanIdStore = new AsyncLocalStorage();
1807
2244
  function currentSpanId() {
1808
2245
  return spanIdStore.getStore();
@@ -1821,12 +2258,26 @@ function formatSpanError(error) {
1821
2258
  }
1822
2259
  return String(error);
1823
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
+ }
1824
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`. */
1825
2273
  name;
2274
+ /** Every transport a written record is sent to. */
1826
2275
  transports;
2276
+ /** Every plugin registered via `.use()`, in registration order. */
1827
2277
  plugins;
1828
2278
  currentLevel;
1829
2279
  baseMeta;
2280
+ dispatchQueue;
1830
2281
  constructor(name, options = {}) {
1831
2282
  this.name = name;
1832
2283
  this.currentLevel = parseLevel(options.level ?? 20 /* INFO */);
@@ -1836,10 +2287,13 @@ var Logger = class _Logger {
1836
2287
  this.use(plugin);
1837
2288
  }
1838
2289
  this.baseMeta = options.meta ? { ...options.meta } : {};
2290
+ this.dispatchQueue = new DispatchQueue(options.queue);
1839
2291
  }
2292
+ /** This logger's current minimum level — records below it are filtered before any plugin runs. */
1840
2293
  get level() {
1841
2294
  return this.currentLevel;
1842
2295
  }
2296
+ /** Changes the minimum level records must meet to reach a transport. Accepts a `Level`, its numeric weight, or its name. */
1843
2297
  setLevel(level) {
1844
2298
  this.currentLevel = parseLevel(level);
1845
2299
  }
@@ -1853,20 +2307,41 @@ var Logger = class _Logger {
1853
2307
  this.plugins.push(typeof plugin === "function" ? new FunctionPlugin(plugin) : plugin);
1854
2308
  return this;
1855
2309
  }
1856
- /** Close every attached transport. Call on shutdown to flush buffered writes. */
1857
- 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();
1858
2331
  for (const transport of this.transports) {
1859
2332
  transport.close();
1860
2333
  }
1861
2334
  }
1862
- /** 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. */
1863
2336
  child(name, meta = {}) {
1864
- return new _Logger(`${this.name}.${name}`, {
2337
+ const child = new _Logger(`${this.name}.${name}`, {
1865
2338
  level: this.currentLevel,
1866
2339
  transports: this.transports,
1867
2340
  plugins: this.plugins,
1868
2341
  meta: { ...this.baseMeta, ...meta }
1869
2342
  });
2343
+ child.dispatchQueue = this.dispatchQueue;
2344
+ return child;
1870
2345
  }
1871
2346
  notifyError(plugin, error, record) {
1872
2347
  try {
@@ -1882,7 +2357,7 @@ var Logger = class _Logger {
1882
2357
  level,
1883
2358
  logger: this.name,
1884
2359
  message,
1885
- meta: { ...this.baseMeta, ...meta }
2360
+ meta: { ...this.baseMeta, ...currentContext(), ...withStackFromErr(meta) }
1886
2361
  });
1887
2362
  const parentSpanId = currentSpanId();
1888
2363
  if (parentSpanId !== void 0) {
@@ -1901,6 +2376,12 @@ var Logger = class _Logger {
1901
2376
  }
1902
2377
  record = result;
1903
2378
  }
2379
+ this.dispatchQueue.enqueue(() => {
2380
+ this.writeAndNotify(record);
2381
+ });
2382
+ return record;
2383
+ }
2384
+ writeAndNotify(record) {
1904
2385
  for (const transport of this.transports) {
1905
2386
  try {
1906
2387
  transport.write(transport.format(record), record);
@@ -1915,23 +2396,28 @@ var Logger = class _Logger {
1915
2396
  this.notifyError(plugin, error, record);
1916
2397
  }
1917
2398
  }
1918
- return record;
1919
2399
  }
2400
+ /** Logs at `TRACE` — the lowest level, for fine-grained diagnostic detail. Returns the record, or `null` if filtered/dropped. */
1920
2401
  trace(message, meta = {}) {
1921
2402
  return this.dispatch(5 /* TRACE */, message, meta);
1922
2403
  }
2404
+ /** Logs at `DEBUG`. Returns the record, or `null` if filtered/dropped. */
1923
2405
  debug(message, meta = {}) {
1924
2406
  return this.dispatch(10 /* DEBUG */, message, meta);
1925
2407
  }
2408
+ /** Logs at `INFO`. Returns the record, or `null` if filtered/dropped. */
1926
2409
  info(message, meta = {}) {
1927
2410
  return this.dispatch(20 /* INFO */, message, meta);
1928
2411
  }
2412
+ /** Logs at `WARN`. Returns the record, or `null` if filtered/dropped. */
1929
2413
  warn(message, meta = {}) {
1930
2414
  return this.dispatch(30 /* WARN */, message, meta);
1931
2415
  }
2416
+ /** Logs at `ERROR`. Returns the record, or `null` if filtered/dropped. */
1932
2417
  error(message, meta = {}) {
1933
2418
  return this.dispatch(40 /* ERROR */, message, meta);
1934
2419
  }
2420
+ /** Logs at `FATAL` — the highest level, for errors that precede an unrecoverable failure. Returns the record, or `null` if filtered/dropped. */
1935
2421
  fatal(message, meta = {}) {
1936
2422
  return this.dispatch(50 /* FATAL */, message, meta);
1937
2423
  }
@@ -1997,9 +2483,71 @@ var Logger = class _Logger {
1997
2483
  }
1998
2484
  };
1999
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
+
2000
2548
  // src/index.ts
2001
2549
  var VERSION = "0.3.0";
2002
2550
 
2003
- 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, RunPlugin, SQLiteTransport, SQSTransport, SamplingPlugin, SlackAlertPlugin, TamperEvidentPlugin, TraceContextPlugin, Transport, VERSION, createRecord, defaultResolveActiveOtelTraceId, generateTraceId, getTraceparent, levelName, parseLevel, parseTraceHeader, setTraceparent, 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 };
2004
2552
  //# sourceMappingURL=index.mjs.map
2005
2553
  //# sourceMappingURL=index.mjs.map