logquill 0.2.0 → 0.3.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.d.cts CHANGED
@@ -54,6 +54,21 @@ interface Plugin {
54
54
  /** Called when one of this plugin's own hooks throws. */
55
55
  onError?(error: unknown, record: LogRecord): void;
56
56
  }
57
+ /** A plain `beforeLog`-style function, as accepted by `Logger.use()` in place of a `Plugin`. */
58
+ type MiddlewareFunc = (record: LogRecord) => LogRecord | null;
59
+ /**
60
+ * Wraps a plain `beforeLog`-style function as a `Plugin`. `Logger.use()`
61
+ * builds one of these automatically when given a function instead of a
62
+ * `Plugin` — Express/Koa-style middleware ergonomics, without needing to
63
+ * read the `Plugin` interface first. There's no `next()` chaining: the
64
+ * pipeline already calls hooks in sequence, so this is sugar for a
65
+ * single-method `Plugin`, not a new execution model.
66
+ */
67
+ declare class FunctionPlugin implements Plugin {
68
+ private readonly func;
69
+ constructor(func: MiddlewareFunc);
70
+ beforeLog(record: LogRecord): LogRecord | null;
71
+ }
57
72
 
58
73
  /**
59
74
  * Injects fixed key/value pairs into every record's `meta`.
@@ -78,15 +93,44 @@ declare class RedactPlugin implements Plugin {
78
93
  beforeLog(record: LogRecord): LogRecord;
79
94
  }
80
95
 
81
- interface SamplingPluginOptions {
82
- rng?: () => number;
96
+ /**
97
+ * Syntactic (not semantic) patterns — matched on shape, so both false
98
+ * positives (a random 9-digit number) and false negatives (anything that
99
+ * doesn't look like these shapes) are expected. Override via `patterns`
100
+ * for anything stricter.
101
+ */
102
+ declare const DEFAULT_PII_PATTERNS: Readonly<Record<string, RegExp>>;
103
+ interface PIIRedactPluginOptions {
104
+ patterns?: Readonly<Record<string, RegExp>>;
105
+ replacement?: string;
83
106
  }
84
- /** Keeps roughly `rate` of records (0.0-1.0), dropping the rest. */
85
- declare class SamplingPlugin implements Plugin {
86
- readonly rate: number;
87
- private readonly rng;
88
- constructor(rate: number, options?: SamplingPluginOptions);
89
- beforeLog(record: LogRecord): LogRecord | null;
107
+ /**
108
+ * Regex-based PII redaction over `meta` **values**, not just keys.
109
+ *
110
+ * Complements `RedactPlugin`, which redacts by exact key match —
111
+ * `PIIRedactPlugin` scans string values (recursively through nested
112
+ * objects/arrays) for emails, SSNs, credit-card numbers, and phone
113
+ * numbers, and redacts matches wherever they appear, regardless of which
114
+ * key holds them (a `notes` field containing a stray SSN is still caught).
115
+ *
116
+ * Detection is pattern-based: fast and dependency-free, but it matches on
117
+ * syntactic shape, not meaning — a random 9-digit number can false-positive
118
+ * as an SSN, and anything that doesn't fit these shapes (a name, a street
119
+ * address) is a false negative. Pass your own `patterns` to extend or
120
+ * replace the defaults.
121
+ *
122
+ * Recursion into nested `meta` structures is depth- and cycle-bounded, so a
123
+ * circular reference or a pathologically deep structure can't hang or
124
+ * crash the caller — it's left unredacted past the bound rather than
125
+ * throwing.
126
+ */
127
+ declare class PIIRedactPlugin implements Plugin {
128
+ readonly patterns: Readonly<Record<string, RegExp>>;
129
+ readonly replacement: string;
130
+ constructor(options?: PIIRedactPluginOptions);
131
+ beforeLog(record: LogRecord): LogRecord;
132
+ private redactValue;
133
+ private redactText;
90
134
  }
91
135
 
92
136
  /**
@@ -110,6 +154,252 @@ declare class CollectingTransport extends Transport {
110
154
  close(): void;
111
155
  }
112
156
 
157
+ interface SamplingPluginOptions {
158
+ rng?: () => number;
159
+ /** `meta` key holding the trace/run id used to group buffered records. Default `"traceId"`. */
160
+ traceKey?: string;
161
+ /** A record at or above this level elevates its whole trace. Default `Level.ERROR`. */
162
+ elevateAt?: LevelInput;
163
+ /** Enables tail-based elevation, writing straight to these transports on elevation. Omit to disable and get plain rate-based sampling. */
164
+ transports?: Transport[];
165
+ /** Total buffered records allowed across every trace before the oldest trace is evicted. Default 1000. */
166
+ maxBufferedRecords?: number;
167
+ /** Distinct trace ids held at once before the oldest is evicted. Default 200. */
168
+ maxTraces?: number;
169
+ }
170
+ /**
171
+ * Keeps roughly `rate` of records (0.0-1.0), dropping the rest.
172
+ *
173
+ * With `transports` set, sampling becomes tail-based per trace: a record
174
+ * that would otherwise be dropped is buffered under its `meta[traceKey]`
175
+ * value instead of discarded outright. If any later record sharing that
176
+ * trace id reaches `elevateAt` or above, the whole trace is "elevated" —
177
+ * every buffered record for that trace id is flushed straight to
178
+ * `transports`, and every subsequent record for that trace id ships
179
+ * unconditionally. This is what lets a sampled-out request still produce a
180
+ * complete trace once it turns out to matter (it errored).
181
+ *
182
+ * Flushing writes buffered records directly to `transports` — pass the
183
+ * same array given to the `Logger`. This bypasses `beforeLog`/`afterLog`/
184
+ * `onError` for any plugin *after* `SamplingPlugin` in the pipeline (the
185
+ * plugins before it already ran, since that's how the buffered record was
186
+ * built); put `SamplingPlugin` last if that matters for your pipeline.
187
+ *
188
+ * Without `transports`, tail-based elevation is inactive and this behaves
189
+ * exactly like plain rate-based sampling (the original behavior) — a
190
+ * record without `meta[traceKey]` is also just rate-sampled, since there's
191
+ * no trace to buffer it under.
192
+ *
193
+ * Buffering is bounded: at most `maxBufferedRecords` records total and
194
+ * `maxTraces` distinct trace ids are held at once. Once either limit is
195
+ * hit, the oldest buffered trace is evicted (and its records are lost, not
196
+ * flushed) — a deliberate bounded-memory trade-off, not a bug: an
197
+ * unbounded per-trace buffer would let a single pathologically long-lived
198
+ * or high-cardinality trace grow memory without limit.
199
+ */
200
+ declare class SamplingPlugin implements Plugin {
201
+ readonly rate: number;
202
+ readonly traceKey: string;
203
+ readonly elevateAt: Level;
204
+ readonly transports: Transport[] | undefined;
205
+ readonly maxBufferedRecords: number;
206
+ readonly maxTraces: number;
207
+ private readonly rng;
208
+ private readonly buffer;
209
+ private bufferedCount;
210
+ private readonly elevated;
211
+ constructor(rate: number, options?: SamplingPluginOptions);
212
+ beforeLog(record: LogRecord): LogRecord | null;
213
+ private elevate;
214
+ private bufferRecord;
215
+ private evictOldestTrace;
216
+ }
217
+
218
+ declare const GENESIS_HASH: string;
219
+ interface TamperEvidentPluginOptions {
220
+ genesisHash?: string;
221
+ }
222
+ /**
223
+ * Hash-chains every record so tampering with a written log can be
224
+ * detected after the fact.
225
+ *
226
+ * Each record gets `meta.hash` — a SHA-256 hex digest over the record's
227
+ * own content plus the previous record's hash (`meta.prevHash`) — the same
228
+ * hash-chain construction used by tamper-evident/append-only logs: editing
229
+ * or deleting any one line breaks every hash after it in the chain, even
230
+ * if the tamperer edits the file directly and not through this plugin.
231
+ * Opt-in — hashing every record has a real, measurable CPU cost, so it
232
+ * isn't part of the default pipeline.
233
+ *
234
+ * Verify a previously-written log with `TamperEvidentPlugin.verifyChain`,
235
+ * which re-derives each record's hash from its content and confirms it
236
+ * matches both the stored `meta.hash` and the chain built from the records
237
+ * before it, in order.
238
+ */
239
+ declare class TamperEvidentPlugin implements Plugin {
240
+ private readonly genesisHash;
241
+ private lastHash;
242
+ constructor(options?: TamperEvidentPluginOptions);
243
+ beforeLog(record: LogRecord): LogRecord;
244
+ /**
245
+ * Returns `true` iff every record's hash matches its content plus the
246
+ * previous record's hash, in the given order. Returns `false` at the
247
+ * first break in the chain (an edited, removed, or reordered record).
248
+ */
249
+ static verifyChain(records: Iterable<Pick<LogRecord, "timestamp" | "level" | "logger" | "message" | "meta">>, options?: TamperEvidentPluginOptions): boolean;
250
+ }
251
+
252
+ interface AlertingPluginOptions {
253
+ /** A record at or above this level fires an alert. Default `Level.ERROR`. */
254
+ threshold?: LevelInput;
255
+ /** How long a dedupe window stays open before a collapsed follow-up alert (if any) fires. Default 300_000 (5 minutes). */
256
+ dedupeWindowMs?: number;
257
+ /** Groups records into the same dedupe window. Default: `level:logger:message`. */
258
+ dedupeKey?: (record: LogRecord) => string;
259
+ /** Distinct concurrent dedupe keys tracked at once; beyond this, new keys are dropped rather than tracked. Default 500. */
260
+ maxTrackedKeys?: number;
261
+ }
262
+ /**
263
+ * Base class for plugins that fire an external alert on ERROR/FATAL (or any
264
+ * configurable `threshold`).
265
+ *
266
+ * A concrete subclass implements only `sendAlert(record, occurrences)` —
267
+ * everything else (thresholding, deduplication, never blocking the caller,
268
+ * never letting a broken destination crash logging) lives here.
269
+ *
270
+ * The first record at or above `threshold` for a given dedupe key (by
271
+ * default: level + logger + message) fires `sendAlert` right away, without
272
+ * awaiting it — so the log call that triggered it is never blocked on a
273
+ * webhook, SMTP handshake, or any other I/O, even if the destination is
274
+ * slow or unreachable. This stands in for the shared async dispatch queue
275
+ * a later phase will introduce; once that queue exists, `AlertingPlugin`
276
+ * can route through it instead of firing its own unawaited call per alert.
277
+ *
278
+ * Any further record matching the same dedupe key within
279
+ * `dedupeWindowMs` of the first is *not* sent again — it just increments a
280
+ * counter. When the window closes, if more than one record matched,
281
+ * exactly one follow-up alert is sent with the total occurrence count,
282
+ * instead of spamming the destination once per record. Tracking is bounded
283
+ * to `maxTrackedKeys` distinct concurrent dedupe keys; beyond that, new
284
+ * keys are dropped rather than tracked (alerting degrades under extreme
285
+ * cardinality, logging itself never does).
286
+ *
287
+ * `sendAlert` is always called without being awaited, and any rejection is
288
+ * routed to this plugin's own `onError`, the same as any other plugin hook
289
+ * that throws.
290
+ */
291
+ declare abstract class AlertingPlugin implements Plugin {
292
+ readonly threshold: Level;
293
+ readonly dedupeWindowMs: number;
294
+ readonly maxTrackedKeys: number;
295
+ private readonly dedupeKeyFn;
296
+ private readonly windows;
297
+ constructor(options?: AlertingPluginOptions);
298
+ afterLog(record: LogRecord): void;
299
+ private flush;
300
+ private safeSend;
301
+ /**
302
+ * Send one alert for `record`, representing `occurrences` collapsed
303
+ * duplicates (1 on first occurrence; the deduped total on a follow-up
304
+ * flush). Override in a concrete subclass — never call this directly,
305
+ * `AlertingPlugin` calls it without awaiting it.
306
+ */
307
+ protected abstract sendAlert(record: LogRecord, occurrences: number): void | Promise<void>;
308
+ onError?(error: unknown, record: LogRecord): void;
309
+ /** Cancel any pending dedupe-window timers. Call on logger shutdown. */
310
+ close(): void;
311
+ }
312
+
313
+ /** Posts one alert body to a Slack incoming webhook URL. Swap in a fake for tests. */
314
+ type SlackSender = (webhookUrl: string, body: string) => Promise<void> | void;
315
+ interface SlackAlertPluginOptions extends AlertingPluginOptions {
316
+ sender?: SlackSender;
317
+ }
318
+ /**
319
+ * Sends deduplicated `AlertingPlugin` alerts to a Slack incoming webhook.
320
+ *
321
+ * `webhookUrl` is the full "Incoming Webhook" URL from Slack's app config.
322
+ * Uses `fetch` — no extra dependency required. Pass `sender` to swap in a
323
+ * fake for tests or an alternate backend.
324
+ */
325
+ declare class SlackAlertPlugin extends AlertingPlugin {
326
+ readonly webhookUrl: string;
327
+ private readonly sender;
328
+ constructor(webhookUrl: string, options?: SlackAlertPluginOptions);
329
+ protected sendAlert(record: LogRecord, occurrences: number): Promise<void>;
330
+ }
331
+
332
+ /** POSTs one PagerDuty Events API v2 payload. Swap in a fake for tests. */
333
+ type PagerDutySender = (body: string) => Promise<void> | void;
334
+ interface PagerDutyAlertPluginOptions extends AlertingPluginOptions {
335
+ sender?: PagerDutySender;
336
+ }
337
+ /**
338
+ * Sends deduplicated `AlertingPlugin` alerts to PagerDuty via the Events
339
+ * API v2 (`POST https://events.pagerduty.com/v2/enqueue`).
340
+ *
341
+ * `routingKey` is an Events API v2 integration key from a PagerDuty
342
+ * service. Uses `fetch` — no extra dependency required. Pass `sender` to
343
+ * swap in a fake for tests or an alternate backend.
344
+ */
345
+ declare class PagerDutyAlertPlugin extends AlertingPlugin {
346
+ readonly routingKey: string;
347
+ private readonly sender;
348
+ constructor(routingKey: string, options?: PagerDutyAlertPluginOptions);
349
+ protected sendAlert(record: LogRecord, occurrences: number): Promise<void>;
350
+ }
351
+
352
+ interface EmailMessage {
353
+ from: string;
354
+ to: string[];
355
+ subject: string;
356
+ text: string;
357
+ }
358
+ /** Sends one email message. Swap in a fake for tests. */
359
+ type EmailSender = (message: EmailMessage) => Promise<void> | void;
360
+ /** The subset of a `nodemailer` transporter that `EmailAlertPlugin` needs. */
361
+ interface NodemailerTransporterLike {
362
+ sendMail(message: {
363
+ from: string;
364
+ to: string;
365
+ subject: string;
366
+ text: string;
367
+ }): Promise<unknown>;
368
+ }
369
+ interface EmailAlertPluginOptions extends AlertingPluginOptions {
370
+ smtpHost: string;
371
+ smtpPort: number;
372
+ fromAddr: string;
373
+ toAddrs: string[];
374
+ username?: string;
375
+ password?: string;
376
+ /** `false` for an SMTP server that doesn't support STARTTLS (e.g. a local relay). Default `true`. */
377
+ useTls?: boolean;
378
+ /** Pre-built sender, e.g. for tests. Skips the `nodemailer` auto-import entirely. */
379
+ sender?: EmailSender;
380
+ }
381
+ /**
382
+ * Sends deduplicated `AlertingPlugin` alerts by email over SMTP.
383
+ *
384
+ * `nodemailer` is an optional peer dependency: install it yourself, or
385
+ * pass `sender` to swap in a fake for tests or an alternate backend —
386
+ * `username`/`password` are only used if both are set.
387
+ */
388
+ declare class EmailAlertPlugin extends AlertingPlugin {
389
+ readonly smtpHost: string;
390
+ readonly smtpPort: number;
391
+ readonly fromAddr: string;
392
+ readonly toAddrs: string[];
393
+ private readonly username;
394
+ private readonly password;
395
+ private readonly useTls;
396
+ private readonly injectedSender;
397
+ private transporter;
398
+ constructor(options: EmailAlertPluginOptions);
399
+ protected sendAlert(record: LogRecord, occurrences: number): Promise<void>;
400
+ private importTransporter;
401
+ }
402
+
113
403
  interface BatchingTransportOptions {
114
404
  formatter?: Formatter;
115
405
  /** Flush once the buffer holds this many items. Default 100. */
@@ -946,7 +1236,7 @@ declare class HTTPTransport extends Transport {
946
1236
  interface LoggerOptions {
947
1237
  level?: LevelInput;
948
1238
  transports?: Transport[];
949
- plugins?: Plugin[];
1239
+ plugins?: (Plugin | MiddlewareFunc)[];
950
1240
  meta?: Record<string, unknown>;
951
1241
  }
952
1242
  declare class Logger {
@@ -958,8 +1248,13 @@ declare class Logger {
958
1248
  constructor(name: string, options?: LoggerOptions);
959
1249
  get level(): Level;
960
1250
  setLevel(level: LevelInput): void;
961
- /** Register a plugin. Returns `this` so calls can be chained. */
962
- use(plugin: Plugin): this;
1251
+ /**
1252
+ * Register a plugin, or a plain `beforeLog`-style function. A function is
1253
+ * wrapped internally as an anonymous `Plugin` (`FunctionPlugin`) — the
1254
+ * same middleware ergonomics as Express/Koa, without needing to read the
1255
+ * `Plugin` interface first. Returns `this` so calls can be chained.
1256
+ */
1257
+ use(plugin: Plugin | MiddlewareFunc): this;
963
1258
  /** Close every attached transport. Call on shutdown to flush buffered writes. */
964
1259
  close(): void;
965
1260
  /** A logger scoped under this one, inheriting its level, transports, and plugins. */
@@ -974,6 +1269,6 @@ declare class Logger {
974
1269
  fatal(message: string, meta?: Record<string, unknown>): LogRecord | null;
975
1270
  }
976
1271
 
977
- declare const VERSION = "0.2.0";
1272
+ declare const VERSION = "0.3.0";
978
1273
 
979
- export { type AmqpChannelLike, type AppInsightsClientLike, type AppInsightsTrace, AppInsightsTransport, type AppInsightsTransportOptions, BaseQueueTransport, type BaseQueueTransportOptions, BaseSQLTransport, type BaseSQLTransportOptions, BatchingTransport, type BatchingTransportOptions, type CloudLoggingClientLike, type CloudLoggingEntry, CloudLoggingTransport, type CloudLoggingTransportOptions, type CloudWatchClientLike, type CloudWatchLogEvent, CloudWatchTransport, type CloudWatchTransportOptions, CollectingTransport, type ConsoleLike, ConsoleTransport, type ConsoleTransportOptions, ContextPlugin, DEFAULT_REDACTED_KEYS, type DatadogSender, DatadogTransport, type DatadogTransportOptions, type DynamoClientLike, DynamoDBTransport, type DynamoDBTransportOptions, type DynamoLogItem, type ElasticsearchSender, ElasticsearchTransport, type ElasticsearchTransportOptions, FileTransport, type FileTransportOptions, type Formatter, HTTPTransport, type HTTPTransportOptions, JSONFormatter, type KafkaProducerLike, KafkaTransport, type KafkaTransportOptions, Level, type LevelInput, type LogRecord, Logger, type LoggerOptions, type MongoClientLike, type MongoCollectionLike, MongoDBTransport, type MongoDBTransportOptions, type MySQLClientLike, MySQLTransport, type MySQLTransportOptions, type NewRelicRegion, type NewRelicSender, type NewRelicSenderResult, NewRelicTransport, type NewRelicTransportOptions, type PgClientLike, type Plugin, PostgresTransport, type PostgresTransportOptions, type PubSubTopicLike, PubSubTransport, type PubSubTransportOptions, RabbitMQTransport, type RabbitMQTransportOptions, RedactPlugin, type RedactPluginOptions, type RedisClientLike, RedisTransport, type RedisTransportOptions, type SQLLogRow, type SQLiteClientLike, type SQLiteStatementLike, SQLiteTransport, type SQLiteTransportOptions, type SQSClientLike, SQSTransport, type SQSTransportOptions, SamplingPlugin, type SamplingPluginOptions, type Sender, Transport, VERSION, createRecord, levelName, parseLevel, utcTimestamp };
1274
+ export { AlertingPlugin, type AlertingPluginOptions, type AmqpChannelLike, type AppInsightsClientLike, type AppInsightsTrace, AppInsightsTransport, type AppInsightsTransportOptions, BaseQueueTransport, type BaseQueueTransportOptions, BaseSQLTransport, type BaseSQLTransportOptions, BatchingTransport, type BatchingTransportOptions, type CloudLoggingClientLike, type CloudLoggingEntry, CloudLoggingTransport, type CloudLoggingTransportOptions, type CloudWatchClientLike, type CloudWatchLogEvent, CloudWatchTransport, type CloudWatchTransportOptions, CollectingTransport, type ConsoleLike, ConsoleTransport, type ConsoleTransportOptions, ContextPlugin, DEFAULT_PII_PATTERNS, DEFAULT_REDACTED_KEYS, type DatadogSender, DatadogTransport, type DatadogTransportOptions, type DynamoClientLike, DynamoDBTransport, type DynamoDBTransportOptions, type DynamoLogItem, type ElasticsearchSender, ElasticsearchTransport, type ElasticsearchTransportOptions, EmailAlertPlugin, type EmailAlertPluginOptions, type EmailMessage, type EmailSender, FileTransport, type FileTransportOptions, type Formatter, FunctionPlugin, GENESIS_HASH, HTTPTransport, type HTTPTransportOptions, JSONFormatter, type KafkaProducerLike, KafkaTransport, type KafkaTransportOptions, Level, type LevelInput, type LogRecord, Logger, type LoggerOptions, type MiddlewareFunc, type MongoClientLike, type MongoCollectionLike, MongoDBTransport, type MongoDBTransportOptions, type MySQLClientLike, MySQLTransport, type MySQLTransportOptions, type NewRelicRegion, type NewRelicSender, type NewRelicSenderResult, NewRelicTransport, type NewRelicTransportOptions, type NodemailerTransporterLike, PIIRedactPlugin, type PIIRedactPluginOptions, PagerDutyAlertPlugin, type PagerDutyAlertPluginOptions, type PagerDutySender, type PgClientLike, type Plugin, PostgresTransport, type PostgresTransportOptions, type PubSubTopicLike, PubSubTransport, type PubSubTransportOptions, RabbitMQTransport, type RabbitMQTransportOptions, RedactPlugin, type RedactPluginOptions, type RedisClientLike, RedisTransport, type RedisTransportOptions, type SQLLogRow, type SQLiteClientLike, type SQLiteStatementLike, SQLiteTransport, type SQLiteTransportOptions, type SQSClientLike, SQSTransport, type SQSTransportOptions, SamplingPlugin, type SamplingPluginOptions, type Sender, SlackAlertPlugin, type SlackAlertPluginOptions, type SlackSender, TamperEvidentPlugin, type TamperEvidentPluginOptions, Transport, VERSION, createRecord, levelName, parseLevel, utcTimestamp };