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 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, the plugin pipeline (with `ContextPlugin`/`RedactPlugin`/
14
- `SamplingPlugin` built in), `JSONFormatter`, and the built-in transports are
15
- implemented; non-blocking async dispatch is not yet — see `CHANGELOG.md`
16
- for what's landed so far.
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
- - **Pluggable transports** — `ConsoleTransport` (colorized, respects `NO_COLOR`, isomorphic), `FileTransport` (rotation), `HTTPTransport` (batched, `fetch`-based); write your own by subclassing `Transport`; `CollectingTransport` ships as an in-memory sink, handy for tests
24
- - **Plugin pipeline** — `ContextPlugin`, `RedactPlugin`, `SamplingPlugin` out of the box; `beforeLog`/`afterLog`/`onError` hooks; a throwing plugin can't crash logging
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