logquill 0.1.1 → 0.2.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 +349 -2
- package/dist/index.cjs +1446 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +978 -2
- package/dist/index.d.ts +978 -2
- package/dist/index.mjs +1412 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +53 -1
package/README.md
CHANGED
|
@@ -2,12 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/nikhilvdev/logquill-js/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/logquill)
|
|
5
|
+
[](https://www.npmjs.com/package/logquill)
|
|
5
6
|
[](./LICENSE)
|
|
6
7
|
|
|
7
8
|
A logging framework for Node/TypeScript that shares one mental model and one
|
|
8
9
|
JSON log shape with its Python sibling, [`logquill`](https://pypi.org/project/logquill/)
|
|
9
10
|
(repo: `logquill-python`).
|
|
10
11
|
|
|
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.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Structured by default** — every call carries a `meta` object, not just a message string
|
|
21
|
+
- **Cross-language record shape** — identical JSON shape and level names/weights as [`logquill` on PyPI](https://pypi.org/project/logquill/)
|
|
22
|
+
- **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
|
|
25
|
+
- **Child loggers** — `.child()` inherits level, transports, and plugins, and merges its own `meta` on top
|
|
26
|
+
- **Typed throughout** — TypeScript strict mode, no `any` in the public API
|
|
27
|
+
- **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
|
+
|
|
11
30
|
## Install
|
|
12
31
|
|
|
13
32
|
```sh
|
|
@@ -17,14 +36,338 @@ npm install logquill
|
|
|
17
36
|
## Usage
|
|
18
37
|
|
|
19
38
|
```ts
|
|
20
|
-
import {
|
|
39
|
+
import { Level, Logger } from "logquill";
|
|
40
|
+
|
|
41
|
+
const logger = new Logger("app", { level: Level.INFO });
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
const record = logger.info("user signed up", { user_id: 42, plan: "pro" });
|
|
44
|
+
console.log(record);
|
|
45
|
+
// { timestamp: '2026-08-29T07:12:55.968Z', level: 'INFO', logger: 'app',
|
|
46
|
+
// message: 'user signed up', meta: { user_id: 42, plan: 'pro' } }
|
|
47
|
+
|
|
48
|
+
logger.debug("below threshold, dropped"); // -> null, filtered by level
|
|
49
|
+
logger.setLevel("debug");
|
|
50
|
+
logger.debug("now visible"); // -> a record
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every log call returns the record (or `null` if filtered by level) —
|
|
54
|
+
`{"timestamp": ISO8601, "level": string, "logger": string, "message": string, "meta": object}`,
|
|
55
|
+
the same shape shared with [`logquill` on PyPI](https://pypi.org/project/logquill/).
|
|
56
|
+
Use `JSONFormatter` to serialize a record to the canonical JSON line:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { JSONFormatter } from "logquill";
|
|
60
|
+
|
|
61
|
+
console.log(new JSONFormatter().format(record));
|
|
62
|
+
// '{"timestamp":"2026-08-29T07:12:55.968Z","level":"INFO","logger":"app","message":"user signed up","meta":{"user_id":42,"plan":"pro"}}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`.child()` creates a logger scoped under this one, inheriting its level,
|
|
66
|
+
transports, and plugins, and merging its own `meta` on top:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const dbLogger = logger.child("db", { component: "pool" });
|
|
70
|
+
dbLogger.warn("connection lost");
|
|
71
|
+
// logger: "app.db", meta: { component: "pool" }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`.use()` registers a plugin implementing `beforeLog`/`afterLog`/`onError`
|
|
75
|
+
hooks (all optional; a throwing hook is caught and routed to `onError`
|
|
76
|
+
rather than crashing logging):
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
logger.use({
|
|
80
|
+
beforeLog(record) {
|
|
81
|
+
return { ...record, message: record.message.toUpperCase() };
|
|
82
|
+
},
|
|
83
|
+
});
|
|
23
84
|
```
|
|
24
85
|
|
|
25
86
|
Works from both ESM (`import`) and CommonJS (`require`) — the package ships
|
|
26
87
|
a dual build with full TypeScript types.
|
|
27
88
|
|
|
89
|
+
## Transports
|
|
90
|
+
|
|
91
|
+
Attach transports to a `Logger` to actually write records somewhere. Each
|
|
92
|
+
record is dispatched to every attached transport synchronously
|
|
93
|
+
(non-blocking dispatch isn't implemented yet):
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { ConsoleTransport, FileTransport, HTTPTransport, Logger } from "logquill";
|
|
97
|
+
|
|
98
|
+
const logger = new Logger("app", {
|
|
99
|
+
transports: [
|
|
100
|
+
new ConsoleTransport(), // console.log; ERROR/FATAL to console.error, colorized
|
|
101
|
+
new FileTransport("app.log", { maxBytes: 10 * 1024 * 1024, backupCount: 5 }),
|
|
102
|
+
new HTTPTransport("https://logs.example.com/ingest", { batchSize: 50 }),
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
logger.info("user signed up", { user_id: 42 });
|
|
107
|
+
logger.close(); // flushes the HTTPTransport's pending batch, closes the FileTransport's fd
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`ConsoleTransport` writes via `console.log`/`console.error` (not
|
|
111
|
+
`process.stdout`/`stderr`), so it works unmodified in a browser bundle.
|
|
112
|
+
Colorizing defaults to on, unless the [`NO_COLOR`](https://no-color.org)
|
|
113
|
+
environment variable is set. `FileTransport` rotates the file once it
|
|
114
|
+
exceeds `maxBytes`, keeping `backupCount` numbered backups (`.1`, `.2`, …).
|
|
115
|
+
`HTTPTransport` batches formatted lines and POSTs them as
|
|
116
|
+
newline-delimited JSON once `batchSize` is reached, or on `.close()`/
|
|
117
|
+
`.flush()`; pass `sender` to swap in a fake for tests or a different
|
|
118
|
+
backend.
|
|
119
|
+
|
|
120
|
+
Write your own by subclassing `Transport` (implement `write(formatted,
|
|
121
|
+
record)`; `format()` and `close()` have defaults), or use
|
|
122
|
+
`CollectingTransport` — an in-memory sink included for tests:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { CollectingTransport } from "logquill";
|
|
126
|
+
|
|
127
|
+
const sink = new CollectingTransport();
|
|
128
|
+
const logger2 = new Logger("app", { transports: [sink] });
|
|
129
|
+
logger2.info("hello");
|
|
130
|
+
console.log(sink.formatted); // ['{"timestamp":...,"message":"hello",...}']
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### SQL transports
|
|
134
|
+
|
|
135
|
+
`SQLiteTransport`, `PostgresTransport`, and `MySQLTransport` all extend
|
|
136
|
+
`BaseSQLTransport`, which owns a fixed `logs` table schema (`timestamp`,
|
|
137
|
+
`level`, `logger`, `message`, `meta`, plus `runId`/`spanId`/`parentSpanId`/
|
|
138
|
+
`traceId` for future trace correlation) and always batches inserts into one
|
|
139
|
+
parameterized multi-row `INSERT` — never one query per log call. Each
|
|
140
|
+
driver (`better-sqlite3`, `pg`, `mysql2`) is an **optional peer
|
|
141
|
+
dependency** — install only the one you use, or inject a pre-built
|
|
142
|
+
client/pool directly (handy for tests). Production schema/migrations are
|
|
143
|
+
your responsibility; pass `ensureSchema: true` to auto-create the table for
|
|
144
|
+
local dev/test only.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { Logger, SQLiteTransport } from "logquill";
|
|
148
|
+
|
|
149
|
+
const transport = new SQLiteTransport({ filename: "app.db", ensureSchema: true });
|
|
150
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
151
|
+
|
|
152
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
153
|
+
await new Promise((r) => setImmediate(r)); // let the batch flush in this example
|
|
154
|
+
logger.close();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { Logger, PostgresTransport } from "logquill";
|
|
159
|
+
// npm install pg
|
|
160
|
+
|
|
161
|
+
const transport = new PostgresTransport({ connectionString: process.env.DATABASE_URL, maxRecords: 100 });
|
|
162
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
163
|
+
|
|
164
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
165
|
+
logger.close();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { Logger, MySQLTransport } from "logquill";
|
|
170
|
+
// npm install mysql2
|
|
171
|
+
|
|
172
|
+
const transport = new MySQLTransport({ connectionString: "mysql://user:pass@localhost:3306/app" });
|
|
173
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
174
|
+
|
|
175
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
176
|
+
logger.close();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### NoSQL transports
|
|
180
|
+
|
|
181
|
+
`MongoDBTransport`, `DynamoDBTransport`, and `RedisTransport` are all
|
|
182
|
+
optional peer dependencies too — install only the driver you use, or
|
|
183
|
+
inject a pre-built client/collection directly.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { Logger, MongoDBTransport } from "logquill";
|
|
187
|
+
// npm install mongodb
|
|
188
|
+
|
|
189
|
+
const transport = new MongoDBTransport({
|
|
190
|
+
connectionString: "mongodb://localhost:27017",
|
|
191
|
+
database: "app",
|
|
192
|
+
collectionName: "logs",
|
|
193
|
+
});
|
|
194
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
195
|
+
logger.info("user signed up", { userId: "u_123" });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { Logger, DynamoDBTransport } from "logquill";
|
|
200
|
+
// npm install @aws-sdk/client-dynamodb
|
|
201
|
+
|
|
202
|
+
// Partition key is meta.runId (falling back to meta.traceId, then the
|
|
203
|
+
// logger name); sort key is `timestamp`. Batches are chunked to respect
|
|
204
|
+
// BatchWriteItem's 25-item cap.
|
|
205
|
+
const transport = new DynamoDBTransport({ tableName: "app-logs", region: "us-east-1" });
|
|
206
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
207
|
+
logger.info("order placed", { runId: "run-42", orderId: "o_9" });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { Logger, RedisTransport } from "logquill";
|
|
212
|
+
// npm install redis
|
|
213
|
+
|
|
214
|
+
// Writes to a Redis Stream via XADD — a fast local buffer/tail, not a
|
|
215
|
+
// durable system of record.
|
|
216
|
+
const transport = new RedisTransport({ url: "redis://localhost:6379", stream: "app:logs" });
|
|
217
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
218
|
+
logger.info("cache miss", { key: "user:42" });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Message queue transports
|
|
222
|
+
|
|
223
|
+
`KafkaTransport`, `RabbitMQTransport`, `SQSTransport`, and `PubSubTransport`
|
|
224
|
+
all extend `BaseQueueTransport`, which owns the "always batch, never
|
|
225
|
+
publish one message per log call" contract — each only implements
|
|
226
|
+
`publishBatch()` against its own driver's batch-publish API. Every
|
|
227
|
+
transport takes a `topic` option naming the destination (a Kafka topic, a
|
|
228
|
+
RabbitMQ queue, an SQS queue URL, or a Pub/Sub topic, respectively). Each
|
|
229
|
+
driver is an optional peer dependency — install only the one you use, or
|
|
230
|
+
inject a pre-built client.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { Logger, KafkaTransport } from "logquill";
|
|
234
|
+
// npm install kafkajs
|
|
235
|
+
|
|
236
|
+
// Each message is keyed by meta.runId (falling back to meta.traceId), so
|
|
237
|
+
// kafkajs's default partitioner keeps one run/trace on the same partition.
|
|
238
|
+
const transport = new KafkaTransport({ topic: "app-logs", brokers: ["localhost:9092"] });
|
|
239
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
240
|
+
logger.info("order placed", { runId: "run-42", orderId: "o-123" });
|
|
241
|
+
logger.close();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { Logger, RabbitMQTransport } from "logquill";
|
|
246
|
+
// npm install amqplib
|
|
247
|
+
|
|
248
|
+
const transport = new RabbitMQTransport({ topic: "app-logs", url: "amqp://localhost" });
|
|
249
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
250
|
+
logger.warn("low disk space", { host: "worker-3" });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { Logger, SQSTransport } from "logquill";
|
|
255
|
+
// npm install @aws-sdk/client-sqs
|
|
256
|
+
|
|
257
|
+
// SendMessageBatch caps a request at 10 messages; larger flushes are
|
|
258
|
+
// automatically chunked.
|
|
259
|
+
const transport = new SQSTransport({
|
|
260
|
+
topic: "https://sqs.us-east-1.amazonaws.com/123456789012/app-logs",
|
|
261
|
+
region: "us-east-1",
|
|
262
|
+
});
|
|
263
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
264
|
+
logger.error("payment failed", { orderId: "o-123" });
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { Logger, PubSubTransport } from "logquill";
|
|
269
|
+
// npm install @google-cloud/pubsub
|
|
270
|
+
|
|
271
|
+
const transport = new PubSubTransport({ topic: "app-logs", projectId: "my-gcp-project" });
|
|
272
|
+
const logger = new Logger("app", { transports: [transport] });
|
|
273
|
+
logger.info("job completed", { jobId: "j-9" });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Cloud-native transports
|
|
277
|
+
|
|
278
|
+
`CloudWatchTransport`, `CloudLoggingTransport` (GCP), and
|
|
279
|
+
`AppInsightsTransport` (Azure) are SDK-based — each driver is an optional
|
|
280
|
+
peer dependency, or inject a pre-built client. `DatadogTransport`,
|
|
281
|
+
`ElasticsearchTransport`, and `NewRelicTransport` are plain `fetch`-based
|
|
282
|
+
and need no extra dependency at all; pass `sender` to swap in a fake for
|
|
283
|
+
tests or an alternate backend.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { Logger, CloudWatchTransport } from "logquill";
|
|
287
|
+
// npm install @aws-sdk/client-cloudwatch-logs
|
|
288
|
+
|
|
289
|
+
const logger = new Logger("app", {
|
|
290
|
+
transports: [new CloudWatchTransport({ logGroupName: "/my-app", logStreamName: "prod", region: "us-east-1" })],
|
|
291
|
+
});
|
|
292
|
+
logger.info("service started");
|
|
293
|
+
logger.close(); // flushes buffered log events
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { Logger, CloudLoggingTransport } from "logquill";
|
|
298
|
+
// npm install @google-cloud/logging
|
|
299
|
+
|
|
300
|
+
const logger = new Logger("app", {
|
|
301
|
+
transports: [new CloudLoggingTransport({ projectId: "my-gcp-project", logName: "my-app" })],
|
|
302
|
+
});
|
|
303
|
+
logger.info("service started");
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { Logger, AppInsightsTransport } from "logquill";
|
|
308
|
+
// npm install applicationinsights
|
|
309
|
+
|
|
310
|
+
const logger = new Logger("app", {
|
|
311
|
+
transports: [new AppInsightsTransport({ connectionString: process.env.APPINSIGHTS_CONNECTION_STRING })],
|
|
312
|
+
});
|
|
313
|
+
logger.info("service started");
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { Logger, DatadogTransport } from "logquill";
|
|
318
|
+
|
|
319
|
+
const logger = new Logger("app", {
|
|
320
|
+
transports: [new DatadogTransport({ apiKey: process.env.DD_API_KEY!, site: "datadoghq.eu" })],
|
|
321
|
+
});
|
|
322
|
+
logger.info("service started");
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { Logger, ElasticsearchTransport } from "logquill";
|
|
327
|
+
|
|
328
|
+
const logger = new Logger("app", {
|
|
329
|
+
transports: [
|
|
330
|
+
new ElasticsearchTransport({ node: "https://localhost:9200", index: "app-logs", apiKey: process.env.ES_API_KEY }),
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
logger.info("service started");
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { Logger, NewRelicTransport } from "logquill";
|
|
338
|
+
|
|
339
|
+
// `region` selects the ingest host ("US" default, or "EU") — set it
|
|
340
|
+
// explicitly for EU accounts, since a mismatched region is rejected.
|
|
341
|
+
// Batches are gzip-compressed, and a 429 pauses further sends until the
|
|
342
|
+
// `Retry-After` window elapses.
|
|
343
|
+
const logger = new Logger("app", {
|
|
344
|
+
transports: [new NewRelicTransport({ licenseKey: process.env.NEW_RELIC_LICENSE_KEY!, region: "EU" })],
|
|
345
|
+
});
|
|
346
|
+
logger.info("service started");
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Plugins
|
|
350
|
+
|
|
351
|
+
Plugins hook into the pipeline around each log call: `beforeLog(record)` can
|
|
352
|
+
transform a record or return `null` to drop it, `afterLog(record)` runs once
|
|
353
|
+
it's been dispatched to every transport, and `onError(error, record)` catches
|
|
354
|
+
anything a plugin's own hooks throw — a broken plugin can't take down logging.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { ContextPlugin, Logger, RedactPlugin, SamplingPlugin } from "logquill";
|
|
358
|
+
|
|
359
|
+
const logger = new Logger("app");
|
|
360
|
+
logger.use(new ContextPlugin({ service: "api", env: "prod" })); // merged into every record's meta
|
|
361
|
+
logger.use(new RedactPlugin({ keys: ["password", "token"] })); // replaces matching meta values
|
|
362
|
+
logger.use(new SamplingPlugin(0.1)); // keep ~10% of records that reach this point
|
|
363
|
+
|
|
364
|
+
logger.info("login attempt", { user_id: 42, password: "hunter2" });
|
|
365
|
+
// meta: { service: "api", env: "prod", user_id: 42, password: "***" }
|
|
366
|
+
// (unless this call was one of the ~90% sampling dropped, in which case it's null)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Write your own by implementing `Plugin`; every hook is optional.
|
|
370
|
+
|
|
28
371
|
## Development
|
|
29
372
|
|
|
30
373
|
```sh
|
|
@@ -35,6 +378,10 @@ npm run typecheck # tsc --noEmit
|
|
|
35
378
|
npm run coverage # vitest run --coverage
|
|
36
379
|
```
|
|
37
380
|
|
|
381
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the PR workflow, the
|
|
382
|
+
[Code of Conduct](CODE_OF_CONDUCT.md) for community standards, and
|
|
383
|
+
[SECURITY.md](.github/SECURITY.md) for how to report a vulnerability.
|
|
384
|
+
|
|
38
385
|
## License
|
|
39
386
|
|
|
40
387
|
MIT
|