@logtape/logtape 0.5.0-dev.66 → 0.5.0-dev.68

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.
Files changed (2) hide show
  1. package/README.md +4 -535
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -36,9 +36,6 @@ The highlights of LogTape are:
36
36
 
37
37
  - *Dead simple sinks*: You can easily add your own sinks to LogTape.
38
38
 
39
- Currently, LogTape provides only few sinks, but [you can easily add your own
40
- sinks.](#sinks)
41
-
42
39
  ![](./screenshots/web-console.png)
43
40
  ![](./screenshots/terminal-console.png)
44
41
 
@@ -67,536 +64,8 @@ bun add @logtape/logtape # for Bun
67
64
  ~~~~
68
65
 
69
66
 
70
- Quick start
71
- -----------
72
-
73
- Set up LogTape in the entry point of your application (if you are composing
74
- a library, you should not set up LogTape in the library itself; it is up to
75
- the application to set up LogTape):
76
-
77
- ~~~~ typescript
78
- import { configure, getConsoleSink } from "@logtape/logtape";
79
-
80
- await configure({
81
- sinks: { console: getConsoleSink() },
82
- filters: {},
83
- loggers: [
84
- { category: "my-app", level: "debug", sinks: ["console"] }
85
- ]
86
- });
87
- ~~~~
88
-
89
- And then you can use LogTape in your application or library:
90
-
91
- ~~~~ typescript
92
- import { getLogger } from "@logtape/logtape";
93
-
94
- const logger = getLogger(["my-app", "my-module"]);
95
-
96
- export function myFunc(value: number): void {
97
- logger.debug `Hello, ${value}!`;
98
- }
99
- ~~~~
100
-
101
-
102
- How to log
103
- ----------
104
-
105
- There are total 5 log levels: `debug`, `info`, `warning`, `error`, `fatal` (in
106
- the order of verbosity). You can log messages with the following syntax:
107
-
108
- ~~~~ typescript
109
- logger.debug `This is a debug message with ${value}.`;
110
- logger.info `This is an info message with ${value}.`;
111
- logger.warn `This is a warning message with ${value}.`;
112
- logger.error `This is an error message with ${value}.`;
113
- logger.fatal `This is a fatal message with ${value}.`;
114
- ~~~~
115
-
116
- You can also log messages with a function call. In this case, log messages
117
- are structured data:
118
-
119
- ~~~~ typescript
120
- logger.debug("This is a debug message with {value}.", { value });
121
- logger.info("This is an info message with {value}.", { value });
122
- logger.warn("This is a warning message with {value}.", { value });
123
- logger.error("This is an error message with {value}.", { value });
124
- logger.fatal("This is a fatal message with {value}.", { value });
125
- ~~~~
126
-
127
- Sometimes, values to be logged are expensive to compute. In such cases, you
128
- can use a function to defer the computation so that it is only computed when
129
- the log message is actually logged:
130
-
131
- ~~~~ typescript
132
- logger.debug(l => l`This is a debug message with ${computeValue()}.`);
133
- logger.debug("Or you can use a function call: {value}.", () => {
134
- return { value: computeValue() };
135
- });
136
- ~~~~
137
-
138
- When using the function call, the way to log single curly braces `{` is to
139
- double the brace `{{`:
140
-
141
- ~~~~ typescript
142
- logger.debug("This logs {{single}} curly braces.");
143
- ~~~~
144
-
145
-
146
- Categories
147
- ----------
148
-
149
- LogTape uses a hierarchical category system to manage loggers. A category is
150
- a list of strings. For example, `["my-app", "my-module"]` is a category.
151
-
152
- When you log a message, it is dispatched to all loggers whose categories are
153
- prefixes of the category of the logger. For example, if you log a message
154
- with the category `["my-app", "my-module", "my-submodule"]`, it is dispatched
155
- to loggers whose categories are `["my-app", "my-module"]` and `["my-app"]`.
156
-
157
- This behavior allows you to control the verbosity of log messages by setting
158
- the log level of loggers at different levels of the category hierarchy.
159
-
160
- Here's an example of setting log levels for different categories:
161
-
162
- ~~~~ typescript
163
- import { configure, getConsoleSink } from "@logtape/logtape";
164
-
165
- await configure({
166
- sinks: {
167
- console: getConsoleSink(),
168
- },
169
- filters: {},
170
- loggers: [
171
- { category: ["my-app"], level: "info", sinks: ["console"] },
172
- { category: ["my-app", "my-module"], level: "debug", sinks: ["console"] },
173
- ],
174
- })
175
- ~~~~
176
-
177
-
178
- Contexts
179
- --------
180
-
181
- *Contexts are available since LogTape 0.5.0.*
182
-
183
- LogTape provides a context system to reuse the same properties across log
184
- messages. A context is a key-value map. You can set a context for a logger
185
- and log messages with the context. Here's an example of setting a context
186
- for a logger:
187
-
188
- ~~~~ typescript
189
- const logger = getLogger(["my-app", "my-module"]);
190
- const ctx = logger.with({ userId: 1234, requestId: "abc" });
191
- ctx.info `This log message will have the context (userId & requestId).`;
192
- ctx.warn("Context can be used inside message template: {userId}, {requestId}.");
193
- ~~~~
194
-
195
- The context is inherited by child loggers. Here's an example of setting a
196
- context for a parent logger and logging messages with a child logger:
197
-
198
- ~~~~ typescript
199
- const logger = getLogger(["my-app"]);
200
- const parentCtx = logger.with({ userId: 1234, requestId: "abc" });
201
- const childCtx = parentCtx.getLogger(["my-module"]);
202
- childCtx.debug("This log message will have the context: {userId} {requestId}.");
203
- ~~~~
204
-
205
- Contexts are particularly useful when you want to do structured logging.
206
-
207
-
208
- Sinks
209
- -----
210
-
211
- A sink is a destination of log messages. LogTape currently provides a few
212
- sinks: console and stream. However, you can easily add your own sinks.
213
- The signature of a sink is:
214
-
215
- ~~~~ typescript
216
- export type Sink = (record: LogRecord) => void;
217
- ~~~~
218
-
219
- Here's a simple example of a sink that writes log messages to console:
220
-
221
- ~~~~ typescript
222
- import { configure } from "@logtape/logtape";
223
-
224
- await configure({
225
- sinks: {
226
- console(record) {
227
- console.log(record.message);
228
- }
229
- },
230
- // Omitted for brevity
231
- });
232
- ~~~~
233
-
234
- ### Console sink
235
-
236
- Of course, you don't have to implement your own console sink because LogTape
237
- provides a console sink:
238
-
239
- ~~~~ typescript
240
- import { configure, getConsoleSink } from "@logtape/logtape";
241
-
242
- await configure({
243
- sinks: {
244
- console: getConsoleSink(),
245
- },
246
- // Omitted for brevity
247
- });
248
- ~~~~
249
-
250
- See also [`getConsoleSink()`] function and [`ConsoleSinkOptions`] interface
251
- in the API reference for more details.
252
-
253
- [`getConsoleSink()`]: https://jsr.io/@logtape/logtape/doc/~/getConsoleSink
254
- [`ConsoleSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/ConsoleSinkOptions
255
-
256
- ### Stream sink
257
-
258
- Another built-in sink is a stream sink. It writes log messages to
259
- a [`WritableStream`]. Here's an example of a stream sink that writes log
260
- messages to the standard error:
261
-
262
- ~~~~ typescript
263
- // Deno:
264
- await configure({
265
- sinks: {
266
- stream: getStreamSink(Deno.stderr.writable),
267
- },
268
- // Omitted for brevity
269
- });
270
- ~~~~
271
-
272
- ~~~~ typescript
273
- // Node.js:
274
- import stream from "node:stream";
275
-
276
- await configure({
277
- sinks: {
278
- stream: getStreamSink(stream.Writable.toWeb(process.stderr)),
279
- },
280
- // Omitted for brevity
281
- });
282
- ~~~~
283
-
284
- > [!NOTE]
285
- > Here we use `WritableStream` from the Web Streams API. If you are using
286
- > Node.js, you cannot directly pass `process.stderr` to `getStreamSink` because
287
- > `process.stderr` is not a `WritableStream` but a [`Writable`], which is a
288
- > Node.js stream. You can use [`Writable.toWeb()`] method to convert a Node.js
289
- > stream to a `WritableStream`.
290
-
291
- See also [`getStreamSink()`] function and [`StreamSinkOptions`] interface
292
- in the API reference for more details.
293
-
294
- [`WritableStream`]: https://developer.mozilla.org/en-US/docs/Web/API/WritableStream
295
- [`Writable`]: https://nodejs.org/api/stream.html#class-streamwritable
296
- [`Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable
297
- [`getStreamSink()`]: https://jsr.io/@logtape/logtape/doc/~/getStreamSink
298
- [`StreamSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/StreamSinkOptions
299
-
300
- ### File sink
301
-
302
- > [!NOTE]
303
- > File sink is unavailable in the browser environment.
304
-
305
- LogTape provides a file sink as well. Here's an example of a file sink that
306
- writes log messages to a file:
307
-
308
- ~~~~ typescript
309
- import { getFileSink } from "@logtape/logtape";
310
-
311
- await configure({
312
- sinks: {
313
- file: getFileSink("my-app.log"),
314
- },
315
- // Omitted for brevity
316
- });
317
- ~~~~
318
-
319
- See also [`getFileSink()`] function and [`FileSinkOptions`] interface
320
- in the API reference for more details.
321
-
322
- > [!NOTE]
323
- > On Deno, you need to have the `--allow-write` flag and the `--unstable-fs`
324
- > flag to use the file sink.
325
-
326
- [`getFileSink()`]: https://jsr.io/@logtape/logtape/doc/~/getFileSink
327
- [`FileSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/FileSinkOptions
328
-
329
- ### Rotating file sink
330
-
331
- > [!NOTE]
332
- > Rotating file sink is unavailable in the browser environment.
333
-
334
- A rotating file sink is a file sink that rotates log files. It creates a new
335
- log file when the current log file reaches a certain size. Here's an example
336
- of a rotating file sink that writes log messages to a file:
337
-
338
- ~~~~ typescript
339
- import { getRotatingFileSink } from "@logtape/logtape";
340
-
341
- await configure({
342
- sinks: {
343
- file: getRotatingFileSink("my-app.log", {
344
- maxFileSize: 1024 * 1024, // 1 MiB
345
- maxFiles: 5,
346
- }),
347
- },
348
- // Omitted for brevity
349
- });
350
- ~~~~
351
-
352
- Rotated log files are named with a suffix like *.1*, *.2*, *.3*, and so on.
353
-
354
- For more details, see [`getRotatingFileSink()`] function and
355
- [`RotatingFileSinkOptions`] interface in the API reference.
356
-
357
- > [!NOTE]
358
- > On Deno, you need to have the `--allow-write` flag and the `--unstable-fs`
359
- > flag to use the rotating file sink.
360
-
361
- [`getRotatingFileSink()`]: https://jsr.io/@logtape/logtape/doc/~/getRotatingFileSink
362
- [`RotatingFileSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/RotatingFileSinkOptions
363
-
364
- ### Text formatter
365
-
366
- A stream sink and a file sink write log messages in a plain text format.
367
- You can customize the format by providing a text formatter. The type of a
368
- text formatter is:
369
-
370
- ~~~~ typescript
371
- export type TextFormatter = (record: LogRecord) => string;
372
- ~~~~
373
-
374
- Here's an example of a text formatter that writes log messages in a [JSON Lines]
375
- format:
376
-
377
- ~~~~ typescript
378
- await configure({
379
- sinks: {
380
- stream: getFileSink("log.jsonl", {
381
- formatter(log) {
382
- return JSON.stringify(log) + "\n",
383
- }
384
- }),
385
- },
386
- // Omitted for brevity
387
- })
388
- ~~~~
389
-
390
- > [!TIP]
391
- > If you want to monitor log messages formatted in JSON Lines in real-time
392
- > readably, you can utilize the `tail` and [`jq`] commands:
393
- >
394
- > ~~~~ sh
395
- > tail -f log.jsonl | jq .
396
- > ~~~~
397
-
398
- [JSON Lines]: https://jsonlines.org/
399
- [`jq`]: https://jqlang.github.io/jq/
400
-
401
- ### OpenTelemetry sink
402
-
403
- If you have an [OpenTelemetry] collector running, you can use the OpenTelemetry
404
- sink to send log messages to the collector using [@logtape/otel] package.
405
-
406
- For more details, see the documentation of [@logtape/otel].
407
-
408
- [OpenTelemetry]: https://opentelemetry.io/
409
- [@logtape/otel]: https://github.com/dahlia/logtape-otel
410
-
411
- ### Disposable sink
412
-
413
- > [!TIP]
414
- > If you are unfamiliar with the concept of disposables, see also the proposal
415
- > of *[ECMAScript Explicit Resource Management]*.
416
-
417
- A disposable sink is a sink that can be disposed of. They are automatically
418
- disposed of when the configuration is reset or the program exits. The type
419
- of a disposable sink is: `Sink & Disposable`. You can create a disposable
420
- sink by defining a `[Symbol.dispose]` method:
421
-
422
- ~~~~ typescript
423
- const disposableSink: Sink & Disposable = (record: LogRecord) => {
424
- console.log(record.message);
425
- };
426
- disposableSink[Symbol.dispose] = () => {
427
- console.log("Disposed!");
428
- };
429
- ~~~~
430
-
431
- A sink can be asynchronously disposed of as well. The type of an asynchronous
432
- disposable sink is: `Sink & AsyncDisposable`. You can create an asynchronous
433
- disposable sink by defining a `[Symbol.asyncDispose]` method:
434
-
435
- ~~~~ typescript
436
- const asyncDisposableSink: Sink & AsyncDisposable = (record: LogRecord) => {
437
- console.log(record.message);
438
- };
439
- asyncDisposableSink[Symbol.asyncDispose] = async () => {
440
- console.log("Disposed!");
441
- };
442
- ~~~~
443
-
444
- [ECMAScript Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
445
-
446
- ### Explicit disposal
447
-
448
- You can explicitly dispose of a sink by calling the [`dispose()`] method. It is
449
- useful when you want to flush the buffer of a sink without blocking returning
450
- a response in edge functions. Here's an example of using the `dispose()`
451
- with [`ctx.waitUntil()`] in Cloudflare Workers:
452
-
453
- ~~~~ typescript
454
- import { configure, dispose } from "@logtape/logtape";
455
-
456
- export default {
457
- async fetch(request, env, ctx) {
458
- await configure({ /* ... */ });
459
- // ...
460
- ctx.waitUntil(dispose());
461
- }
462
- }
463
- ~~~~
464
-
465
- [`dispose()`]: https://jsr.io/@logtape/logtape/doc/~/dispose
466
- [`ctx.waitUntil()`]: https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil
467
-
468
-
469
- Filters
470
- -------
471
-
472
- A filter is a function that filters log messages. A filter takes a log record
473
- and returns a boolean value. If the filter returns `true`, the log record is
474
- passed to the sinks; otherwise, the log record is discarded. Its signature is:
475
-
476
- ~~~~ typescript
477
- export type Filter = (record: LogRecord) => boolean;
478
- ~~~~
479
-
480
- For example, the following filter discards log messages whose property `elapsed`
481
- is less than 100 milliseconds:
482
-
483
- ~~~~ typescript
484
- import { configure, type LogRecord } from "@logtape/logtape";
485
-
486
- await configure({
487
- // Omitted for brevity
488
- filters: {
489
- tooSlow(record: LogRecord) {
490
- return "elapsed" in record.properties && record.properties.elapsed >= 100;
491
- },
492
- },
493
- loggers: [
494
- {
495
- category: ["my-app", "database"],
496
- level: "debug",
497
- sinks: ["console"],
498
- filters: ["tooSlow"],
499
- }
500
- ]
501
- });
502
- ~~~~
503
-
504
- ### Level filter
505
-
506
- LogTape provides a built-in level filter. You can use the level filter to
507
- filter log messages by their log levels. The level filter factory takes
508
- a [`LogLevel`] string and returns a level filter. For example, the following
509
- level filter discards log messages whose log level is less than `info`:
510
-
511
- ~~~~ typescript
512
- import { getLevelFilter } from "@logtape/logtape";
513
-
514
- await configure({
515
- filters: {
516
- infoOrHigher: getLevelFilter("info");
517
- },
518
- // Omitted for brevity
519
- });
520
- ~~~~
521
-
522
- [`LogLevel`]: https://jsr.io/@logtape/logtape/doc/~/LogLevel
523
-
524
- ### Sink filter
525
-
526
- A sink filter is a filter that is applied to a specific sink. You can add a
527
- sink filter to a sink by decorating the sink with [`withFilter()`]:
528
-
529
- ~~~~ typescript
530
- import { getConsoleSink, withFilter } from "@logtape/logtape";
531
-
532
- await configure({
533
- sinks: {
534
- filteredConsole: withFilter(
535
- getConsoleSink(),
536
- log => "elapsed" in log.properties && log.properties.elapsed >= 100,
537
- ),
538
- },
539
- // Omitted for brevity
540
- });
541
- ~~~~
542
-
543
- The `filteredConsoleSink` only logs messages whose property `elapsed` is greater
544
- than or equal to 100 milliseconds to the console.
545
-
546
- > [!TIP]
547
- > The `withFilter()` function can take a [`LogLevel`] string as the second
548
- > argument. In this case, the log messages whose log level is less than
549
- > the specified log level are discarded.
550
-
551
- [`withFilter()`]: https://jsr.io/@logtape/logtape/doc/~/withFilter
552
-
553
-
554
- Testing
555
- -------
556
-
557
- Here are some tips for testing your application or library with LogTape.
558
-
559
- ### Reset configuration
560
-
561
- You can reset the configuration of LogTape to its initial state. This is
562
- useful when you want to reset the configuration between tests. For example,
563
- the following code shows how to reset the configuration after a test
564
- (regardless of whether the test passes or fails) in Deno:
565
-
566
- ~~~~ typescript
567
- import { configure, reset } from "@logtape/logtape";
568
-
569
- Deno.test("my test", async (t) => {
570
- await t.step("set up", async () => {
571
- await configure({ /* ... */ });
572
- });
573
-
574
- await t.step("run test", () => {
575
- // Run the test
576
- });
577
-
578
- await t.step("tear down", async () => {
579
- await reset();
580
- });
581
- });
582
- ~~~~
583
-
584
- ### Buffer sink
585
-
586
- For testing purposes, you may want to collect log messages in memory. Although
587
- LogTape does not provide a built-in buffer sink, you can easily implement it:
588
-
589
- ~~~~ typescript
590
- import { type LogRecord, configure } from "@logtape/logtape";
591
-
592
- const buffer: LogRecord[] = [];
593
-
594
- await configure({
595
- sinks: {
596
- buffer: buffer.push.bind(buffer),
597
- },
598
- // Omitted for brevity
599
- });
600
- ~~~~
67
+ Docs
68
+ ----
601
69
 
602
- <!-- cSpell: ignore otel -->
70
+ The docs of LogTape is available at <https://logtape.org/>.
71
+ For the API references, see <https://jsr.io/@logtape/logtape>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "0.5.0-dev.66+4db6d7ff",
3
+ "version": "0.5.0-dev.68+ff42f972",
4
4
  "description": "Simple logging library with zero dependencies for Deno/Node.js/Bun/browsers",
5
5
  "keywords": [
6
6
  "logging",
@@ -12,7 +12,7 @@
12
12
  "email": "hong@minhee.org",
13
13
  "url": "https://hongminhee.org/"
14
14
  },
15
- "homepage": "https://github.com/dahlia/logtape",
15
+ "homepage": "https://logtape.org/",
16
16
  "repository": {
17
17
  "type": "git",
18
18
  "url": "git+https://github.com/dahlia/logtape.git"