logquill 0.2.0 → 0.4.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,22 +10,44 @@ 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
- - *(planned)* non-blocking async dispatch, `AsyncLocalStorage`-based context propagation — see `CHANGELOG.md`
29
+ - **Tracing & agentic logging** — `.thought()/.action()/.observation()/.decision()`, `Logger.span()` for nested/durationMs-stamped spans, `RunPlugin` (per-run id + step counter), and `TraceContextPlugin` (cross-service `traceId`, OTel-aware) — see [Tracing & agentic logging](#tracing--agentic-logging)
30
+ - **LangChain.js / LangGraph.js adapter** — `LangChainAdapter`, a `BaseCallbackHandler` that maps chain/LLM/tool/agent events onto the calls above with zero manual instrumentation, from a separate `logquill/langchain` entry point — see [Agentic framework adapters](#agentic-framework-adapters)
31
+ - *(planned)* non-blocking async dispatch, `AsyncLocalStorage`-based general context propagation — see `CHANGELOG.md`
32
+
33
+ ## Contents
34
+
35
+ - [Install](#install)
36
+ - [Usage](#usage)
37
+ - [Transports](#transports)
38
+ - [SQL transports](#sql-transports)
39
+ - [NoSQL transports](#nosql-transports)
40
+ - [Message queue transports](#message-queue-transports)
41
+ - [Cloud-native transports](#cloud-native-transports)
42
+ - [Plugins](#plugins)
43
+ - [Tail-based sampling](#tail-based-sampling)
44
+ - [PII redaction](#pii-redaction)
45
+ - [Tamper-evident logs](#tamper-evident-logs)
46
+ - [Alerting](#alerting)
47
+ - [Tracing & agentic logging](#tracing--agentic-logging)
48
+ - [Agentic framework adapters](#agentic-framework-adapters)
49
+ - [Development](#development)
50
+ - [License](#license)
29
51
 
30
52
  ## Install
31
53
 
@@ -90,7 +112,36 @@ a dual build with full TypeScript types.
90
112
 
91
113
  Attach transports to a `Logger` to actually write records somewhere. Each
92
114
  record is dispatched to every attached transport synchronously
93
- (non-blocking dispatch isn't implemented yet):
115
+ (non-blocking dispatch isn't implemented yet). Every backend driver below
116
+ is an **optional peer dependency** — `npm install` only pulls in what you
117
+ actually import; nothing is installed on your behalf.
118
+
119
+ | Transport | Backend | Peer dependency | Setup |
120
+ |---|---|---|---|
121
+ | `ConsoleTransport` | stdout/stderr via `console.*` | *(none)* | zero-setup, isomorphic |
122
+ | `FileTransport` | local file, with rotation | *(none)* | zero-setup |
123
+ | `HTTPTransport` | any HTTP log endpoint | *(none, uses `fetch`)* | zero-setup |
124
+ | `SQLiteTransport` | SQLite | `better-sqlite3` | zero-setup (file or `:memory:`) |
125
+ | `PostgresTransport` | PostgreSQL | `pg` | needs a server |
126
+ | `MySQLTransport` | MySQL | `mysql2` | needs a server |
127
+ | `MongoDBTransport` | MongoDB | `mongodb` | needs a server |
128
+ | `DynamoDBTransport` | AWS DynamoDB | `@aws-sdk/client-dynamodb` | needs an AWS account |
129
+ | `RedisTransport` | Redis Streams | `redis` | needs a server |
130
+ | `KafkaTransport` | Kafka | `kafkajs` | needs a broker |
131
+ | `RabbitMQTransport` | RabbitMQ | `amqplib` | needs a broker |
132
+ | `SQSTransport` | AWS SQS | `@aws-sdk/client-sqs` | needs an AWS account |
133
+ | `PubSubTransport` | GCP Pub/Sub | `@google-cloud/pubsub` | needs a GCP account |
134
+ | `CloudWatchTransport` | AWS CloudWatch Logs | `@aws-sdk/client-cloudwatch-logs` | needs an AWS account |
135
+ | `CloudLoggingTransport` | GCP Cloud Logging | `@google-cloud/logging` | needs a GCP account |
136
+ | `AppInsightsTransport` | Azure Application Insights | `applicationinsights` | needs an Azure account |
137
+ | `DatadogTransport` | Datadog Logs | *(none, uses `fetch`)* | needs a Datadog account |
138
+ | `ElasticsearchTransport` | Elasticsearch `_bulk` API | *(none, uses `fetch`)* | needs a cluster |
139
+ | `NewRelicTransport` | New Relic Log API | *(none, uses `fetch`)* | needs a New Relic account |
140
+
141
+ Every transport also accepts an injected client/sender in place of its
142
+ default driver — the pattern every example below and every transport's own
143
+ test suite uses, so you never need a live backend just to test your
144
+ logging setup.
94
145
 
95
146
  ```ts
96
147
  import { ConsoleTransport, FileTransport, HTTPTransport, Logger } from "logquill";
@@ -366,7 +417,209 @@ logger.info("login attempt", { user_id: 42, password: "hunter2" });
366
417
  // (unless this call was one of the ~90% sampling dropped, in which case it's null)
367
418
  ```
368
419
 
369
- Write your own by implementing `Plugin`; every hook is optional.
420
+ Write your own by implementing `Plugin`; every hook is optional. `.use()`
421
+ also accepts a plain function in place of a `Plugin` — sugar for a
422
+ single-method `beforeLog` plugin, Express/Koa-style:
423
+
424
+ ```ts
425
+ logger.use((record) => {
426
+ delete record.meta.ssn;
427
+ return record; // or null to drop the record
428
+ });
429
+ ```
430
+
431
+ ### Tail-based sampling
432
+
433
+ `SamplingPlugin` can do more than flat-rate sampling: pass `transports`
434
+ and it buffers a sampled-out record under its `meta.traceId` instead of
435
+ dropping it outright. If a later record on that same trace reaches
436
+ `elevateAt` (default `ERROR`), the whole trace is flushed — every buffered
437
+ record for it, plus everything from then on — so a request that turned out
438
+ to matter still produces a complete trace, even though most of its steps
439
+ would otherwise have been sampled away.
440
+
441
+ ```ts
442
+ import { CollectingTransport, Logger, SamplingPlugin } from "logquill";
443
+
444
+ const sink = new CollectingTransport();
445
+ const logger = new Logger("app", {
446
+ transports: [sink],
447
+ plugins: [new SamplingPlugin(0.01, { transports: [sink] })], // keep 1%, but never lose an errored trace
448
+ });
449
+
450
+ logger.info("step 1", { traceId: "req-42" }); // likely dropped...
451
+ logger.info("step 2", { traceId: "req-42" }); // ...and this one too
452
+ logger.error("step 3 failed", { traceId: "req-42" }); // elevates req-42 — steps 1-3 all ship
453
+ ```
454
+
455
+ ### PII redaction
456
+
457
+ `PIIRedactPlugin` complements `RedactPlugin`'s exact-key matching with
458
+ regex-based scanning of `meta` **values** — emails, SSNs, credit-card
459
+ numbers, and phone numbers are redacted wherever they appear, recursively
460
+ through nested objects/arrays, regardless of which key holds them.
461
+
462
+ ```ts
463
+ import { Logger, PIIRedactPlugin } from "logquill";
464
+
465
+ const logger = new Logger("app", { plugins: [new PIIRedactPlugin()] });
466
+ logger.info("support ticket", { notes: "contact me at jane@example.com" });
467
+ // meta.notes: "contact me at ***"
468
+ ```
469
+
470
+ ### Tamper-evident logs
471
+
472
+ `TamperEvidentPlugin` hash-chains every record — each one's `meta.hash` is
473
+ a SHA-256 digest over its own content plus the previous record's hash — so
474
+ editing, removing, or reordering a written line breaks the chain from that
475
+ point on, detectable later with the static `verifyChain()`. Opt-in: hashing
476
+ every record has a real CPU cost.
477
+
478
+ ```ts
479
+ import { Logger, TamperEvidentPlugin } from "logquill";
480
+
481
+ const logger = new Logger("app", { plugins: [new TamperEvidentPlugin()] });
482
+ const records = [logger.info("one"), logger.info("two")];
483
+
484
+ TamperEvidentPlugin.verifyChain(records); // true
485
+ ```
486
+
487
+ ### Alerting
488
+
489
+ `AlertingPlugin` is the base for plugins that fire an external alert on
490
+ ERROR/FATAL (or any configurable `threshold`) without ever blocking the
491
+ log call that triggered it. Repeated matches within a dedupe window
492
+ collapse into a single follow-up alert reporting the total count, instead
493
+ of spamming the destination once per record.
494
+
495
+ ```ts
496
+ import { Logger, SlackAlertPlugin } from "logquill";
497
+
498
+ const logger = new Logger("app", {
499
+ plugins: [new SlackAlertPlugin("https://hooks.slack.com/services/...")],
500
+ });
501
+ logger.error("payment webhook failed", { orderId: "o-123" });
502
+ ```
503
+
504
+ `PagerDutyAlertPlugin` (Events API v2, no extra dependency) and
505
+ `EmailAlertPlugin` (SMTP via the optional `nodemailer` peer dependency, or
506
+ inject a `sender`) follow the same shape. Write your own by extending
507
+ `AlertingPlugin` and implementing `sendAlert(record, occurrences)`.
508
+
509
+ ## Tracing & agentic logging
510
+
511
+ `.thought()/.action()/.observation()/.decision()` are `.info()` with
512
+ `meta.kind` pre-set, for tagging steps of an agent's reasoning loop:
513
+
514
+ ```ts
515
+ import { Logger } from "logquill";
516
+
517
+ const logger = new Logger("agent");
518
+ logger.thought("deciding which tool to call", { candidates: ["search", "calculator"] });
519
+ logger.action("calling search", { query: "current weather in nyc" });
520
+ logger.observation("search returned 3 results");
521
+ logger.decision("using search result #1");
522
+ ```
523
+
524
+ `Logger.span()` wraps an operation, emitting one record on completion with
525
+ `meta.spanId`/`meta.durationMs` — every record logged inside it, including
526
+ across an `await`, is automatically stamped with `meta.parentSpanId`, so a
527
+ full run's nesting can be reconstructed by sorting on `spanId`/`parentSpanId`.
528
+ It still emits (at `ERROR`, with `meta.error`) and rethrows if the block
529
+ throws:
530
+
531
+ ```ts
532
+ import { Logger } from "logquill";
533
+
534
+ async function callLlm(prompt: string): Promise<string> {
535
+ return `response to: ${prompt}`;
536
+ }
537
+
538
+ const logger = new Logger("agent");
539
+
540
+ const answer = await logger.span(
541
+ "callLlm",
542
+ async () => {
543
+ logger.action("requesting completion", { model: "gpt-4" });
544
+ const response = await callLlm("current weather in nyc");
545
+ logger.observation("received completion");
546
+ return response;
547
+ },
548
+ { model: "gpt-4" },
549
+ );
550
+ ```
551
+
552
+ `RunPlugin` stamps `meta.runId` (generated, or given explicitly) plus an
553
+ incrementing `meta.step` — attach a fresh instance per run so concurrent
554
+ runs don't share a counter:
555
+
556
+ ```ts
557
+ import { Logger, RunPlugin } from "logquill";
558
+
559
+ const runLogger = new Logger("app").child("agent").use(new RunPlugin());
560
+ runLogger.thought("step one"); // meta: { kind: "thought", runId: "...", step: 0 }
561
+ runLogger.action("step two"); // meta: { kind: "action", runId: "...", step: 1 }
562
+ ```
563
+
564
+ `TraceContextPlugin` stamps `meta.traceId`, for correlating one request
565
+ across services — distinct from `runId`, which scopes one agent run. It
566
+ resolves, in priority order: an active OpenTelemetry span (if
567
+ `@opentelemetry/api` is installed — never a required dependency), an
568
+ inbound `traceparent`/X-Ray/GCP trace header, or a freshly generated id:
569
+
570
+ ```ts
571
+ import { Logger, setTraceparent, TraceContextPlugin } from "logquill";
572
+
573
+ const logger = new Logger("app", { plugins: [new TraceContextPlugin()] });
574
+
575
+ // in HTTP middleware, before the handler runs:
576
+ const reset = setTraceparent(req.headers["traceparent"]);
577
+ try {
578
+ logger.info("handling request"); // meta.traceId resolved from the inbound header
579
+ } finally {
580
+ reset();
581
+ }
582
+ ```
583
+
584
+ ## Agentic framework adapters
585
+
586
+ `LangChainAdapter` implements LangChain.js's `BaseCallbackHandler`, mapping
587
+ chain/LLM/tool/agent events onto `.action()/.observation()/.decision()` and
588
+ `span()`-shaped records — pass it into `callbacks: [...]` and a chain's
589
+ full call tree is captured with zero manual instrumentation:
590
+
591
+ ```ts
592
+ import { RunnableLambda } from "@langchain/core/runnables";
593
+ import { Logger, RunPlugin } from "logquill";
594
+ import { LangChainAdapter } from "logquill/langchain";
595
+
596
+ const logger = new Logger("agent").use(new RunPlugin());
597
+ const handler = new LangChainAdapter(logger);
598
+
599
+ const answerQuestion = RunnableLambda.from((question: string) => `answer: ${question}`);
600
+ await answerQuestion.invoke("what is 2+2?", { callbacks: [handler] });
601
+ // logs one record: { message: "RunnableLambda", meta: { kind: "span", spanId: "...", durationMs: ... } }
602
+ ```
603
+
604
+ `LangGraphAdapter` is the same handler under its own name, for LangGraph.js
605
+ graphs — its nodes run as ordinary LangChain `Runnable`s, so no extra
606
+ mapping is needed; pass it the same way:
607
+
608
+ ```ts
609
+ import { LangGraphAdapter } from "logquill/langchain";
610
+
611
+ const handler = new LangGraphAdapter(logger);
612
+ // const graph = builder.compile({ checkpointer });
613
+ // await graph.invoke(input, { callbacks: [handler], configurable: { thread_id: "1" } });
614
+ ```
615
+
616
+ This is a **separate entry point** — `import ... from "logquill/langchain"`,
617
+ not the main `"logquill"` import — because `LangChainAdapter` has to
618
+ `extends BaseCallbackHandler`, LangChain's own class. Importing plain
619
+ `logquill` never touches `@langchain/core`; only importing
620
+ `logquill/langchain` does. Install `@langchain/core` yourself (it's an
621
+ optional peer dependency) — no separate `@langchain/langgraph` dependency
622
+ is needed for `LangGraphAdapter`.
370
623
 
371
624
  ## Development
372
625