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 CHANGED
@@ -2,12 +2,31 @@
2
2
 
3
3
  [![CI](https://github.com/nikhilvdev/logquill-js/actions/workflows/ci.yml/badge.svg)](https://github.com/nikhilvdev/logquill-js/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/logquill.svg)](https://www.npmjs.com/package/logquill)
5
+ [![npm downloads](https://img.shields.io/npm/dm/logquill.svg)](https://www.npmjs.com/package/logquill)
5
6
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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 { VERSION } from "logquill";
39
+ import { Level, Logger } from "logquill";
40
+
41
+ const logger = new Logger("app", { level: Level.INFO });
21
42
 
22
- console.log(VERSION);
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