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 +262 -9
- package/dist/index.cjs +612 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +371 -103
- package/dist/index.d.ts +371 -103
- package/dist/index.mjs +596 -7
- package/dist/index.mjs.map +1 -1
- package/dist/langchain.cjs +114 -0
- package/dist/langchain.cjs.map +1 -0
- package/dist/langchain.d.cts +122 -0
- package/dist/langchain.d.ts +122 -0
- package/dist/langchain.mjs +110 -0
- package/dist/langchain.mjs.map +1 -0
- package/dist/logger-D1_THnBJ.d.cts +163 -0
- package/dist/logger-D1_THnBJ.d.ts +163 -0
- package/package.json +24 -10
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,
|
|
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
|
+
- **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
|
|