logquill 0.1.2 → 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/README.md +358 -8
- package/dist/index.cjs +1497 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1078 -13
- package/dist/index.d.ts +1078 -13
- package/dist/index.mjs +1470 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +57 -1
package/README.md
CHANGED
|
@@ -10,23 +10,41 @@ JSON log shape with its Python sibling, [`logquill`](https://pypi.org/project/lo
|
|
|
10
10
|
(repo: `logquill-python`).
|
|
11
11
|
|
|
12
12
|
Status: pre-release, under active development. The core `Logger`, level
|
|
13
|
-
filtering,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
filtering, a full plugin pipeline (context/redaction/PII/tamper-evidence/
|
|
14
|
+
sampling/alerting), `JSONFormatter`, and a broad transport catalog —
|
|
15
|
+
console/file/HTTP, SQL, NoSQL, message queues, and cloud-native log
|
|
16
|
+
platforms — are implemented; non-blocking async dispatch is not yet — see
|
|
17
|
+
`CHANGELOG.md` for what's landed so far.
|
|
17
18
|
|
|
18
19
|
## Features
|
|
19
20
|
|
|
20
21
|
- **Structured by default** — every call carries a `meta` object, not just a message string
|
|
21
22
|
- **Cross-language record shape** — identical JSON shape and level names/weights as [`logquill` on PyPI](https://pypi.org/project/logquill/)
|
|
22
23
|
- **Pluggable formatters** — `JSONFormatter` out of the box; implement `format(record) -> string` for your own
|
|
23
|
-
- **
|
|
24
|
-
- **Plugin pipeline** — `ContextPlugin`, `RedactPlugin`, `SamplingPlugin` out of the box; `beforeLog`/`afterLog`/`onError` hooks
|
|
24
|
+
- **A transport for wherever your logs need to go** — console, file, and HTTP out of the box, plus SQL (SQLite/Postgres/MySQL), NoSQL (MongoDB/DynamoDB/Redis), message queues (Kafka/RabbitMQ/SQS/Pub-Sub), and cloud-native platforms (CloudWatch/Cloud Logging/App Insights/Datadog/Elasticsearch/New Relic) — see [Transports](#transports). Every backend driver is an **optional peer dependency**: install only the one you use, or inject a pre-built client. Write your own by subclassing `Transport`; `CollectingTransport` ships as an in-memory sink, handy for tests
|
|
25
|
+
- **Plugin pipeline** — `ContextPlugin`, `RedactPlugin`, `PIIRedactPlugin`, tail-based `SamplingPlugin`, `TamperEvidentPlugin`, and `AlertingPlugin` (`SlackAlertPlugin`/`PagerDutyAlertPlugin`/`EmailAlertPlugin`) out of the box; `beforeLog`/`afterLog`/`onError` hooks, or just pass a plain function to `.use()`; a throwing plugin can't crash logging — see [Plugins](#plugins)
|
|
25
26
|
- **Child loggers** — `.child()` inherits level, transports, and plugins, and merges its own `meta` on top
|
|
26
27
|
- **Typed throughout** — TypeScript strict mode, no `any` in the public API
|
|
27
28
|
- **Dual package** — works via both `require()` (CJS) and `import` (ESM) from the same published package
|
|
28
29
|
- *(planned)* non-blocking async dispatch, `AsyncLocalStorage`-based context propagation — see `CHANGELOG.md`
|
|
29
30
|
|
|
31
|
+
## Contents
|
|
32
|
+
|
|
33
|
+
- [Install](#install)
|
|
34
|
+
- [Usage](#usage)
|
|
35
|
+
- [Transports](#transports)
|
|
36
|
+
- [SQL transports](#sql-transports)
|
|
37
|
+
- [NoSQL transports](#nosql-transports)
|
|
38
|
+
- [Message queue transports](#message-queue-transports)
|
|
39
|
+
- [Cloud-native transports](#cloud-native-transports)
|
|
40
|
+
- [Plugins](#plugins)
|
|
41
|
+
- [Tail-based sampling](#tail-based-sampling)
|
|
42
|
+
- [PII redaction](#pii-redaction)
|
|
43
|
+
- [Tamper-evident logs](#tamper-evident-logs)
|
|
44
|
+
- [Alerting](#alerting)
|
|
45
|
+
- [Development](#development)
|
|
46
|
+
- [License](#license)
|
|
47
|
+
|
|
30
48
|
## Install
|
|
31
49
|
|
|
32
50
|
```sh
|
|
@@ -90,7 +108,36 @@ a dual build with full TypeScript types.
|
|
|
90
108
|
|
|
91
109
|
Attach transports to a `Logger` to actually write records somewhere. Each
|
|
92
110
|
record is dispatched to every attached transport synchronously
|
|
93
|
-
(non-blocking dispatch isn't implemented yet)
|
|
111
|
+
(non-blocking dispatch isn't implemented yet). Every backend driver below
|
|
112
|
+
is an **optional peer dependency** — `npm install` only pulls in what you
|
|
113
|
+
actually import; nothing is installed on your behalf.
|
|
114
|
+
|
|
115
|
+
| Transport | Backend | Peer dependency | Setup |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `ConsoleTransport` | stdout/stderr via `console.*` | *(none)* | zero-setup, isomorphic |
|
|
118
|
+
| `FileTransport` | local file, with rotation | *(none)* | zero-setup |
|
|
119
|
+
| `HTTPTransport` | any HTTP log endpoint | *(none, uses `fetch`)* | zero-setup |
|
|
120
|
+
| `SQLiteTransport` | SQLite | `better-sqlite3` | zero-setup (file or `:memory:`) |
|
|
121
|
+
| `PostgresTransport` | PostgreSQL | `pg` | needs a server |
|
|
122
|
+
| `MySQLTransport` | MySQL | `mysql2` | needs a server |
|
|
123
|
+
| `MongoDBTransport` | MongoDB | `mongodb` | needs a server |
|
|
124
|
+
| `DynamoDBTransport` | AWS DynamoDB | `@aws-sdk/client-dynamodb` | needs an AWS account |
|
|
125
|
+
| `RedisTransport` | Redis Streams | `redis` | needs a server |
|
|
126
|
+
| `KafkaTransport` | Kafka | `kafkajs` | needs a broker |
|
|
127
|
+
| `RabbitMQTransport` | RabbitMQ | `amqplib` | needs a broker |
|
|
128
|
+
| `SQSTransport` | AWS SQS | `@aws-sdk/client-sqs` | needs an AWS account |
|
|
129
|
+
| `PubSubTransport` | GCP Pub/Sub | `@google-cloud/pubsub` | needs a GCP account |
|
|
130
|
+
| `CloudWatchTransport` | AWS CloudWatch Logs | `@aws-sdk/client-cloudwatch-logs` | needs an AWS account |
|
|
131
|
+
| `CloudLoggingTransport` | GCP Cloud Logging | `@google-cloud/logging` | needs a GCP account |
|
|
132
|
+
| `AppInsightsTransport` | Azure Application Insights | `applicationinsights` | needs an Azure account |
|
|
133
|
+
| `DatadogTransport` | Datadog Logs | *(none, uses `fetch`)* | needs a Datadog account |
|
|
134
|
+
| `ElasticsearchTransport` | Elasticsearch `_bulk` API | *(none, uses `fetch`)* | needs a cluster |
|
|
135
|
+
| `NewRelicTransport` | New Relic Log API | *(none, uses `fetch`)* | needs a New Relic account |
|
|
136
|
+
|
|
137
|
+
Every transport also accepts an injected client/sender in place of its
|
|
138
|
+
default driver — the pattern every example below and every transport's own
|
|
139
|
+
test suite uses, so you never need a live backend just to test your
|
|
140
|
+
logging setup.
|
|
94
141
|
|
|
95
142
|
```ts
|
|
96
143
|
import { ConsoleTransport, FileTransport, HTTPTransport, Logger } from "logquill";
|
|
@@ -130,6 +177,222 @@ logger2.info("hello");
|
|
|
130
177
|
console.log(sink.formatted); // ['{"timestamp":...,"message":"hello",...}']
|
|
131
178
|
```
|
|
132
179
|
|
|
180
|
+
### SQL transports
|
|
181
|
+
|
|
182
|
+
`SQLiteTransport`, `PostgresTransport`, and `MySQLTransport` all extend
|
|
183
|
+
`BaseSQLTransport`, which owns a fixed `logs` table schema (`timestamp`,
|
|
184
|
+
`level`, `logger`, `message`, `meta`, plus `runId`/`spanId`/`parentSpanId`/
|
|
185
|
+
`traceId` for future trace correlation) and always batches inserts into one
|
|
186
|
+
parameterized multi-row `INSERT` — never one query per log call. Each
|
|
187
|
+
driver (`better-sqlite3`, `pg`, `mysql2`) is an **optional peer
|
|
188
|
+
dependency** — install only the one you use, or inject a pre-built
|
|
189
|
+
client/pool directly (handy for tests). Production schema/migrations are
|
|
190
|
+
your responsibility; pass `ensureSchema: true` to auto-create the table for
|
|
191
|
+
local dev/test only.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { Logger, SQLiteTransport } from "logquill";
|
|
195
|
+
|
|
196
|
+
const transport = new SQLiteTransport({ filename: "app.db", ensureSchema: true });
|
|
197
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
198
|
+
|
|
199
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
200
|
+
await new Promise((r) => setImmediate(r)); // let the batch flush in this example
|
|
201
|
+
logger.close();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { Logger, PostgresTransport } from "logquill";
|
|
206
|
+
// npm install pg
|
|
207
|
+
|
|
208
|
+
const transport = new PostgresTransport({ connectionString: process.env.DATABASE_URL, maxRecords: 100 });
|
|
209
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
210
|
+
|
|
211
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
212
|
+
logger.close();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { Logger, MySQLTransport } from "logquill";
|
|
217
|
+
// npm install mysql2
|
|
218
|
+
|
|
219
|
+
const transport = new MySQLTransport({ connectionString: "mysql://user:pass@localhost:3306/app" });
|
|
220
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
221
|
+
|
|
222
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
223
|
+
logger.close();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### NoSQL transports
|
|
227
|
+
|
|
228
|
+
`MongoDBTransport`, `DynamoDBTransport`, and `RedisTransport` are all
|
|
229
|
+
optional peer dependencies too — install only the driver you use, or
|
|
230
|
+
inject a pre-built client/collection directly.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { Logger, MongoDBTransport } from "logquill";
|
|
234
|
+
// npm install mongodb
|
|
235
|
+
|
|
236
|
+
const transport = new MongoDBTransport({
|
|
237
|
+
connectionString: "mongodb://localhost:27017",
|
|
238
|
+
database: "app",
|
|
239
|
+
collectionName: "logs",
|
|
240
|
+
});
|
|
241
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
242
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { Logger, DynamoDBTransport } from "logquill";
|
|
247
|
+
// npm install @aws-sdk/client-dynamodb
|
|
248
|
+
|
|
249
|
+
// Partition key is meta.runId (falling back to meta.traceId, then the
|
|
250
|
+
// logger name); sort key is `timestamp`. Batches are chunked to respect
|
|
251
|
+
// BatchWriteItem's 25-item cap.
|
|
252
|
+
const transport = new DynamoDBTransport({ tableName: "app-logs", region: "us-east-1" });
|
|
253
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
254
|
+
logger.info("order placed", { runId: "run-42", orderId: "o_9" });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { Logger, RedisTransport } from "logquill";
|
|
259
|
+
// npm install redis
|
|
260
|
+
|
|
261
|
+
// Writes to a Redis Stream via XADD — a fast local buffer/tail, not a
|
|
262
|
+
// durable system of record.
|
|
263
|
+
const transport = new RedisTransport({ url: "redis://localhost:6379", stream: "app:logs" });
|
|
264
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
265
|
+
logger.info("cache miss", { key: "user:42" });
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Message queue transports
|
|
269
|
+
|
|
270
|
+
`KafkaTransport`, `RabbitMQTransport`, `SQSTransport`, and `PubSubTransport`
|
|
271
|
+
all extend `BaseQueueTransport`, which owns the "always batch, never
|
|
272
|
+
publish one message per log call" contract — each only implements
|
|
273
|
+
`publishBatch()` against its own driver's batch-publish API. Every
|
|
274
|
+
transport takes a `topic` option naming the destination (a Kafka topic, a
|
|
275
|
+
RabbitMQ queue, an SQS queue URL, or a Pub/Sub topic, respectively). Each
|
|
276
|
+
driver is an optional peer dependency — install only the one you use, or
|
|
277
|
+
inject a pre-built client.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { Logger, KafkaTransport } from "logquill";
|
|
281
|
+
// npm install kafkajs
|
|
282
|
+
|
|
283
|
+
// Each message is keyed by meta.runId (falling back to meta.traceId), so
|
|
284
|
+
// kafkajs's default partitioner keeps one run/trace on the same partition.
|
|
285
|
+
const transport = new KafkaTransport({ topic: "app-logs", brokers: ["localhost:9092"] });
|
|
286
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
287
|
+
logger.info("order placed", { runId: "run-42", orderId: "o-123" });
|
|
288
|
+
logger.close();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { Logger, RabbitMQTransport } from "logquill";
|
|
293
|
+
// npm install amqplib
|
|
294
|
+
|
|
295
|
+
const transport = new RabbitMQTransport({ topic: "app-logs", url: "amqp://localhost" });
|
|
296
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
297
|
+
logger.warn("low disk space", { host: "worker-3" });
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { Logger, SQSTransport } from "logquill";
|
|
302
|
+
// npm install @aws-sdk/client-sqs
|
|
303
|
+
|
|
304
|
+
// SendMessageBatch caps a request at 10 messages; larger flushes are
|
|
305
|
+
// automatically chunked.
|
|
306
|
+
const transport = new SQSTransport({
|
|
307
|
+
topic: "https://sqs.us-east-1.amazonaws.com/123456789012/app-logs",
|
|
308
|
+
region: "us-east-1",
|
|
309
|
+
});
|
|
310
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
311
|
+
logger.error("payment failed", { orderId: "o-123" });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { Logger, PubSubTransport } from "logquill";
|
|
316
|
+
// npm install @google-cloud/pubsub
|
|
317
|
+
|
|
318
|
+
const transport = new PubSubTransport({ topic: "app-logs", projectId: "my-gcp-project" });
|
|
319
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
320
|
+
logger.info("job completed", { jobId: "j-9" });
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Cloud-native transports
|
|
324
|
+
|
|
325
|
+
`CloudWatchTransport`, `CloudLoggingTransport` (GCP), and
|
|
326
|
+
`AppInsightsTransport` (Azure) are SDK-based — each driver is an optional
|
|
327
|
+
peer dependency, or inject a pre-built client. `DatadogTransport`,
|
|
328
|
+
`ElasticsearchTransport`, and `NewRelicTransport` are plain `fetch`-based
|
|
329
|
+
and need no extra dependency at all; pass `sender` to swap in a fake for
|
|
330
|
+
tests or an alternate backend.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { Logger, CloudWatchTransport } from "logquill";
|
|
334
|
+
// npm install @aws-sdk/client-cloudwatch-logs
|
|
335
|
+
|
|
336
|
+
const logger = new Logger("app", {
|
|
337
|
+
transports: [new CloudWatchTransport({ logGroupName: "/my-app", logStreamName: "prod", region: "us-east-1" })],
|
|
338
|
+
});
|
|
339
|
+
logger.info("service started");
|
|
340
|
+
logger.close(); // flushes buffered log events
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { Logger, CloudLoggingTransport } from "logquill";
|
|
345
|
+
// npm install @google-cloud/logging
|
|
346
|
+
|
|
347
|
+
const logger = new Logger("app", {
|
|
348
|
+
transports: [new CloudLoggingTransport({ projectId: "my-gcp-project", logName: "my-app" })],
|
|
349
|
+
});
|
|
350
|
+
logger.info("service started");
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { Logger, AppInsightsTransport } from "logquill";
|
|
355
|
+
// npm install applicationinsights
|
|
356
|
+
|
|
357
|
+
const logger = new Logger("app", {
|
|
358
|
+
transports: [new AppInsightsTransport({ connectionString: process.env.APPINSIGHTS_CONNECTION_STRING })],
|
|
359
|
+
});
|
|
360
|
+
logger.info("service started");
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { Logger, DatadogTransport } from "logquill";
|
|
365
|
+
|
|
366
|
+
const logger = new Logger("app", {
|
|
367
|
+
transports: [new DatadogTransport({ apiKey: process.env.DD_API_KEY!, site: "datadoghq.eu" })],
|
|
368
|
+
});
|
|
369
|
+
logger.info("service started");
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { Logger, ElasticsearchTransport } from "logquill";
|
|
374
|
+
|
|
375
|
+
const logger = new Logger("app", {
|
|
376
|
+
transports: [
|
|
377
|
+
new ElasticsearchTransport({ node: "https://localhost:9200", index: "app-logs", apiKey: process.env.ES_API_KEY }),
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
logger.info("service started");
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { Logger, NewRelicTransport } from "logquill";
|
|
385
|
+
|
|
386
|
+
// `region` selects the ingest host ("US" default, or "EU") — set it
|
|
387
|
+
// explicitly for EU accounts, since a mismatched region is rejected.
|
|
388
|
+
// Batches are gzip-compressed, and a 429 pauses further sends until the
|
|
389
|
+
// `Retry-After` window elapses.
|
|
390
|
+
const logger = new Logger("app", {
|
|
391
|
+
transports: [new NewRelicTransport({ licenseKey: process.env.NEW_RELIC_LICENSE_KEY!, region: "EU" })],
|
|
392
|
+
});
|
|
393
|
+
logger.info("service started");
|
|
394
|
+
```
|
|
395
|
+
|
|
133
396
|
## Plugins
|
|
134
397
|
|
|
135
398
|
Plugins hook into the pipeline around each log call: `beforeLog(record)` can
|
|
@@ -150,7 +413,94 @@ logger.info("login attempt", { user_id: 42, password: "hunter2" });
|
|
|
150
413
|
// (unless this call was one of the ~90% sampling dropped, in which case it's null)
|
|
151
414
|
```
|
|
152
415
|
|
|
153
|
-
Write your own by implementing `Plugin`; every hook is optional.
|
|
416
|
+
Write your own by implementing `Plugin`; every hook is optional. `.use()`
|
|
417
|
+
also accepts a plain function in place of a `Plugin` — sugar for a
|
|
418
|
+
single-method `beforeLog` plugin, Express/Koa-style:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
logger.use((record) => {
|
|
422
|
+
delete record.meta.ssn;
|
|
423
|
+
return record; // or null to drop the record
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Tail-based sampling
|
|
428
|
+
|
|
429
|
+
`SamplingPlugin` can do more than flat-rate sampling: pass `transports`
|
|
430
|
+
and it buffers a sampled-out record under its `meta.traceId` instead of
|
|
431
|
+
dropping it outright. If a later record on that same trace reaches
|
|
432
|
+
`elevateAt` (default `ERROR`), the whole trace is flushed — every buffered
|
|
433
|
+
record for it, plus everything from then on — so a request that turned out
|
|
434
|
+
to matter still produces a complete trace, even though most of its steps
|
|
435
|
+
would otherwise have been sampled away.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { CollectingTransport, Logger, SamplingPlugin } from "logquill";
|
|
439
|
+
|
|
440
|
+
const sink = new CollectingTransport();
|
|
441
|
+
const logger = new Logger("app", {
|
|
442
|
+
transports: [sink],
|
|
443
|
+
plugins: [new SamplingPlugin(0.01, { transports: [sink] })], // keep 1%, but never lose an errored trace
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
logger.info("step 1", { traceId: "req-42" }); // likely dropped...
|
|
447
|
+
logger.info("step 2", { traceId: "req-42" }); // ...and this one too
|
|
448
|
+
logger.error("step 3 failed", { traceId: "req-42" }); // elevates req-42 — steps 1-3 all ship
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### PII redaction
|
|
452
|
+
|
|
453
|
+
`PIIRedactPlugin` complements `RedactPlugin`'s exact-key matching with
|
|
454
|
+
regex-based scanning of `meta` **values** — emails, SSNs, credit-card
|
|
455
|
+
numbers, and phone numbers are redacted wherever they appear, recursively
|
|
456
|
+
through nested objects/arrays, regardless of which key holds them.
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { Logger, PIIRedactPlugin } from "logquill";
|
|
460
|
+
|
|
461
|
+
const logger = new Logger("app", { plugins: [new PIIRedactPlugin()] });
|
|
462
|
+
logger.info("support ticket", { notes: "contact me at jane@example.com" });
|
|
463
|
+
// meta.notes: "contact me at ***"
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Tamper-evident logs
|
|
467
|
+
|
|
468
|
+
`TamperEvidentPlugin` hash-chains every record — each one's `meta.hash` is
|
|
469
|
+
a SHA-256 digest over its own content plus the previous record's hash — so
|
|
470
|
+
editing, removing, or reordering a written line breaks the chain from that
|
|
471
|
+
point on, detectable later with the static `verifyChain()`. Opt-in: hashing
|
|
472
|
+
every record has a real CPU cost.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { Logger, TamperEvidentPlugin } from "logquill";
|
|
476
|
+
|
|
477
|
+
const logger = new Logger("app", { plugins: [new TamperEvidentPlugin()] });
|
|
478
|
+
const records = [logger.info("one"), logger.info("two")];
|
|
479
|
+
|
|
480
|
+
TamperEvidentPlugin.verifyChain(records); // true
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Alerting
|
|
484
|
+
|
|
485
|
+
`AlertingPlugin` is the base for plugins that fire an external alert on
|
|
486
|
+
ERROR/FATAL (or any configurable `threshold`) without ever blocking the
|
|
487
|
+
log call that triggered it. Repeated matches within a dedupe window
|
|
488
|
+
collapse into a single follow-up alert reporting the total count, instead
|
|
489
|
+
of spamming the destination once per record.
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
import { Logger, SlackAlertPlugin } from "logquill";
|
|
493
|
+
|
|
494
|
+
const logger = new Logger("app", {
|
|
495
|
+
plugins: [new SlackAlertPlugin("https://hooks.slack.com/services/...")],
|
|
496
|
+
});
|
|
497
|
+
logger.error("payment webhook failed", { orderId: "o-123" });
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`PagerDutyAlertPlugin` (Events API v2, no extra dependency) and
|
|
501
|
+
`EmailAlertPlugin` (SMTP via the optional `nodemailer` peer dependency, or
|
|
502
|
+
inject a `sender`) follow the same shape. Write your own by extending
|
|
503
|
+
`AlertingPlugin` and implementing `sendAlert(record, occurrences)`.
|
|
154
504
|
|
|
155
505
|
## Development
|
|
156
506
|
|