@logtape/logtape 0.1.0-dev.9 → 0.2.0-dev.19

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
@@ -3,16 +3,391 @@
3
3
  LogTape
4
4
  =======
5
5
 
6
+ [![JSR][JSR badge]][JSR]
7
+ [![npm][npm badge]][npm]
6
8
  [![GitHub Actions][GitHub Actions badge]][GitHub Actions]
7
9
  [![Codecov][Codecov badge]][Codecov]
8
10
 
11
+ > [!NOTE]
12
+ > LogTape is still in the early stage of development. The API is not stable
13
+ > yet. Please be careful when using it in production.
14
+
9
15
  LogTape is a simple logging library for Deno/Node.js/Bun/browsers. It is
10
16
  designed to be used for both applications and libraries.
11
17
 
12
18
  Currently, LogTape provides only few sinks, but you can easily add your own
13
19
  sinks.
14
20
 
21
+ ![](./screenshots/web-console.png)
22
+ ![](./screenshots/terminal-console.png)
23
+
24
+ [JSR]: https://jsr.io/@logtape/logtape
25
+ [JSR badge]: https://jsr.io/badges/@logtape/logtape
26
+ [npm]: https://www.npmjs.com/package/@logtape/logtape
27
+ [npm badge]: https://img.shields.io/npm/v/@logtape/logtape?logo=npm
15
28
  [GitHub Actions]: https://github.com/dahlia/logtape/actions/workflows/main.yaml
16
29
  [GitHub Actions badge]: https://github.com/dahlia/logtape/actions/workflows/main.yaml/badge.svg
17
30
  [Codecov]: https://codecov.io/gh/dahlia/logtape
18
31
  [Codecov badge]: https://codecov.io/gh/dahlia/logtape/graph/badge.svg?token=yOejfcuX7r
32
+
33
+
34
+ Installation
35
+ ------------
36
+
37
+ ### Deno
38
+
39
+ ~~~~ sh
40
+ deno add @logtape/logtape
41
+ ~~~~
42
+
43
+ ### Node.js
44
+
45
+ ~~~~ sh
46
+ npm add @logtape/logtape
47
+ ~~~~
48
+
49
+ ### Bun
50
+
51
+ ~~~~ sh
52
+ bun add @logtape/logtape
53
+ ~~~~
54
+
55
+
56
+ Quick start
57
+ -----------
58
+
59
+ Set up LogTape in the entry point of your application (if you are composing
60
+ a library, you should not set up LogTape in the library itself; it is up to
61
+ the application to set up LogTape):
62
+
63
+ ~~~~ typescript
64
+ import { configure, getConsoleSink } from "@logtape/logtape";
65
+
66
+ await configure({
67
+ sinks: { console: getConsoleSink() },
68
+ filters: {},
69
+ loggers: [
70
+ { category: "my-app", level: "debug", sinks: ["console"] }
71
+ ]
72
+ });
73
+ ~~~~
74
+
75
+ And then you can use LogTape in your application or library:
76
+
77
+ ~~~~ typescript
78
+ import { getLogger } from "@logtape/logtape";
79
+
80
+ const logger = getLogger(["my-app", "my-module"]);
81
+
82
+ export function myFunc(value: number): void {
83
+ logger.debug `Hello, ${value}!`;
84
+ }
85
+ ~~~~
86
+
87
+
88
+ How to log
89
+ ----------
90
+
91
+ There are total 5 log levels: `debug`, `info`, `warning`, `error`, `fatal` (in
92
+ the order of verbosity). You can log messages with the following syntax:
93
+
94
+ ~~~~ typescript
95
+ logger.debug `This is a debug message with ${value}.`;
96
+ logger.info `This is an info message with ${value}.`;
97
+ logger.warn `This is a warning message with ${value}.`;
98
+ logger.error `This is an error message with ${value}.`;
99
+ logger.fatal `This is a fatal message with ${value}.`;
100
+ ~~~~
101
+
102
+ You can also log messages with a function call:
103
+
104
+ ~~~~ typescript
105
+ logger.debug("This is a debug message with {value}.", { value });
106
+ logger.info("This is an info message with {value}.", { value });
107
+ logger.warn("This is a warning message with {value}.", { value });
108
+ logger.error("This is an error message with {value}.", { value });
109
+ logger.fatal("This is a fatal message with {value}.", { value });
110
+ ~~~~
111
+
112
+ Sometimes, values to be logged are expensive to compute. In such cases, you
113
+ can use a function to defer the computation so that it is only computed when
114
+ the log message is actually logged:
115
+
116
+ ~~~~ typescript
117
+ logger.debug(l => l`This is a debug message with ${computeValue()}.`);
118
+ logger.debug("Or you can use a function call: {value}.", () => {
119
+ return { value: computeValue() };
120
+ });
121
+ ~~~~
122
+
123
+
124
+ Categories
125
+ ----------
126
+
127
+ LogTape uses a hierarchical category system to manage loggers. A category is
128
+ a list of strings. For example, `["my-app", "my-module"]` is a category.
129
+
130
+ When you log a message, it is dispatched to all loggers whose categories are
131
+ prefixes of the category of the logger. For example, if you log a message
132
+ with the category `["my-app", "my-module", "my-submodule"]`, it is dispatched
133
+ to loggers whose categories are `["my-app", "my-module"]` and `["my-app"]`.
134
+
135
+ This behavior allows you to control the verbosity of log messages by setting
136
+ the log level of loggers at different levels of the category hierarchy.
137
+
138
+ Here's an example of setting log levels for different categories:
139
+
140
+ ~~~~ typescript
141
+ import { configure, getConsoleSink } from "@logtape/logtape";
142
+
143
+ await configure({
144
+ sinks: {
145
+ console: getConsoleSink(),
146
+ },
147
+ filters: {},
148
+ loggers: [
149
+ { category: ["my-app"], level: "info", sinks: ["console"] },
150
+ { category: ["my-app", "my-module"], level: "debug", sinks: ["console"] },
151
+ ],
152
+ })
153
+ ~~~~
154
+
155
+
156
+ Sinks
157
+ -----
158
+
159
+ A sink is a destination of log messages. LogTape currently provides a few
160
+ sinks: console and stream. However, you can easily add your own sinks.
161
+ The signature of a sink is:
162
+
163
+ ~~~~ typescript
164
+ export type Sink = (record: LogRecord) => void;
165
+ ~~~~
166
+
167
+ Here's a simple example of a sink that writes log messages to console:
168
+
169
+ ~~~~ typescript
170
+ import { configure } from "@logtape/logtape";
171
+
172
+ await configure({
173
+ sinks: {
174
+ console(record) {
175
+ console.log(record.message);
176
+ }
177
+ },
178
+ // Omitted for brevity
179
+ });
180
+ ~~~~
181
+
182
+ ### Console sink
183
+
184
+ Of course, you don't have to implement your own console sink because LogTape
185
+ provides a console sink:
186
+
187
+ ~~~~ typescript
188
+ import { configure, getConsoleSink } from "@logtape/logtape";
189
+
190
+ await configure({
191
+ sinks: {
192
+ console: getConsoleSink(),
193
+ },
194
+ // Omitted for brevity
195
+ });
196
+ ~~~~
197
+
198
+ See also [`getConsoleSink()`] function and [`ConsoleSinkOptions`] interface
199
+ in the API reference for more details.
200
+
201
+ [`getConsoleSink()`]: https://jsr.io/@logtape/logtape/doc/~/getConsoleSink
202
+ [`ConsoleSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/ConsoleSinkOptions
203
+
204
+ ### Stream sink
205
+
206
+ Another built-in sink is a stream sink. It writes log messages to
207
+ a [`WritableStream`]. Here's an example of a stream sink that writes log
208
+ messages to the standard error:
209
+
210
+ ~~~~ typescript
211
+ // Deno:
212
+ await configure({
213
+ sinks: {
214
+ stream: getStreamSink(Deno.stderr.writable),
215
+ },
216
+ // Omitted for brevity
217
+ });
218
+ ~~~~
219
+
220
+ ~~~~ typescript
221
+ // Node.js:
222
+ import stream from "node:stream";
223
+
224
+ await configure({
225
+ sinks: {
226
+ stream: getStreamSink(stream.Writable.toWeb(process.stderr)),
227
+ },
228
+ // Omitted for brevity
229
+ });
230
+ ~~~~
231
+
232
+ > [!NOTE]
233
+ > Here we use `WritableStream` from the Web Streams API. If you are using
234
+ > Node.js, you cannot directly pass `process.stderr` to `getStreamSink` because
235
+ > `process.stderr` is not a `WritableStream` but a [`Writable`], which is a
236
+ > Node.js stream. You can use [`Writable.toWeb()`] method to convert a Node.js
237
+ > stream to a `WritableStream`.
238
+
239
+ See also [`getStreamSink()`] function and [`StreamSinkOptions`] interface
240
+ in the API reference for more details.
241
+
242
+ [`WritableStream`]: https://developer.mozilla.org/en-US/docs/Web/API/WritableStream
243
+ [`Writable`]: https://nodejs.org/api/stream.html#class-streamwritable
244
+ [`Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable
245
+ [`getStreamSink()`]: https://jsr.io/@logtape/logtape/doc/~/getStreamSink
246
+ [`StreamSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/StreamSinkOptions
247
+
248
+ ### File sink
249
+
250
+ > [!NOTE]
251
+ > File sink is unavailable in the browser environment.
252
+
253
+ LogTape provides a file sink as well. Here's an example of a file sink that
254
+ writes log messages to a file:
255
+
256
+ ~~~~ typescript
257
+ import { getFileSink } from "@logtape/logtape";
258
+
259
+ await configure({
260
+ sinks: {
261
+ file: getFileSink("my-app.log"),
262
+ },
263
+ // Omitted for brevity
264
+ });
265
+ ~~~~
266
+
267
+ See also [`getFileSink()`] function and [`FileSinkOptions`] interface
268
+ in the API reference for more details.
269
+
270
+ [`getFileSink()`]: https://jsr.io/@logtape/logtape/doc/~/getFileSink
271
+ [`FileSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/FileSinkOptions
272
+
273
+ ### Text formatter
274
+
275
+ A stream sink and a file sink write log messages in a plain text format.
276
+ You can customize the format by providing a text formatter. The type of a
277
+ text formatter is:
278
+
279
+ ~~~~ typescript
280
+ export type TextFormatter = (record: LogRecord) => string;
281
+ ~~~~
282
+
283
+ Here's an example of a text formatter that writes log messages in a JSON format:
284
+
285
+ ~~~~ typescript
286
+ await configure({
287
+ sinks: {
288
+ stream: getStreamSink(Deno.stderr.writable, {
289
+ formatter: JSON.stringify,
290
+ }),
291
+ },
292
+ // Omitted for brevity
293
+ })
294
+ ~~~~
295
+
296
+ ### Disposable sink
297
+
298
+ A disposable sink is a sink that can be disposed of. They are automatically
299
+ disposed of when the configuration is reset or the program exits. The type
300
+ of a disposable sink is: `Sink & Disposable`. You can create a disposable
301
+ sink by defining a `[Symbol.dispose]` method:
302
+
303
+ ~~~~ typescript
304
+ const disposableSink: Sink & Disposable = (record: LogRecord) => {
305
+ console.log(record.message);
306
+ };
307
+ disposableSink[Symbol.dispose] = () => {
308
+ console.log("Disposed!");
309
+ };
310
+ ~~~~
311
+
312
+ A sync can be asynchronously disposed of as well. The type of an asynchronous
313
+ disposable sink is: `Sink & AsyncDisposable`. You can create an asynchronous
314
+ disposable sink by defining a `[Symbol.asyncDispose]` method:
315
+
316
+ ~~~~ typescript
317
+ const asyncDisposableSink: Sink & AsyncDisposable = (record: LogRecord) => {
318
+ console.log(record.message);
319
+ };
320
+ asyncDisposableSink[Symbol.asyncDispose] = async () => {
321
+ console.log("Disposed!");
322
+ };
323
+ ~~~~
324
+
325
+ ### Explicit disposal
326
+
327
+ You can explicitly dispose of a sink by calling the `dispose()` method. It is
328
+ useful when you want to flush the buffer of a sink without blocking returning
329
+ a response in edge functions. Here's an example of using the `dispose()`
330
+ with [`ctx.waitUntil()`] in Cloudflare Workers:
331
+
332
+ ~~~~ typescript
333
+ import { configure, dispose } from "@logtape/logtape";
334
+
335
+ export default {
336
+ async fetch(request, env, ctx) {
337
+ await configure({ /* ... */ });
338
+ // ...
339
+ ctx.waitUntil(dispose());
340
+ }
341
+ }
342
+ ~~~~
343
+
344
+ [`ctx.waitUntil()`]: https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil
345
+
346
+
347
+ Testing
348
+ -------
349
+
350
+ Here are some tips for testing your application or library with LogTape.
351
+
352
+ ### Reset configuration
353
+
354
+ You can reset the configuration of LogTape to its initial state. This is
355
+ useful when you want to reset the configuration between tests. For example,
356
+ the following code shows how to reset the configuration after a test
357
+ (regardless of whether the test passes or fails) in Deno:
358
+
359
+ ~~~~ typescript
360
+ import { configure, reset } from "@logtape/logtape";
361
+
362
+ Deno.test("my test", async (t) => {
363
+ await t.step("set up", async () => {
364
+ await configure({ /* ... */ });
365
+ });
366
+
367
+ await t.step("run test", () => {
368
+ // Run the test
369
+ });
370
+
371
+ await t.step("tear down", async () => {
372
+ await reset();
373
+ });
374
+ });
375
+ ~~~~
376
+
377
+ ### Buffer sink
378
+
379
+ For testing purposes, you may want to collect log messages in memory. Although
380
+ LogTape does not provide a built-in buffer sink, you can easily implement it:
381
+
382
+ ~~~~ typescript
383
+ import { type LogRecord, configure } from "@logtape/logtape";
384
+
385
+ const buffer: LogRecord[] = [];
386
+
387
+ await configure({
388
+ sinks: {
389
+ buffer: buffer.push.bind(buffer),
390
+ },
391
+ // Omitted for brevity
392
+ });
393
+ ~~~~
package/esm/config.js CHANGED
@@ -1,13 +1,28 @@
1
+ import * as dntShim from "./_dnt.shims.js";
1
2
  import { toFilter } from "./filter.js";
2
3
  import { LoggerImpl } from "./logger.js";
3
4
  import { getConsoleSink } from "./sink.js";
5
+ /**
6
+ * Whether the loggers are configured.
7
+ */
4
8
  let configured = false;
9
+ /**
10
+ * Disposables to dispose when resetting the configuration.
11
+ */
12
+ const disposables = new Set();
13
+ /**
14
+ * Async disposables to dispose when resetting the configuration.
15
+ */
16
+ const asyncDisposables = new Set();
5
17
  /**
6
18
  * Configure the loggers with the specified configuration.
7
19
  *
20
+ * Note that if the given sinks or filters are disposable, they will be
21
+ * disposed when the configuration is reset, or when the process exits.
22
+ *
8
23
  * @example
9
24
  * ```typescript
10
- * configure({
25
+ * await configure({
11
26
  * sinks: {
12
27
  * console: getConsoleSink(),
13
28
  * },
@@ -38,12 +53,12 @@ let configured = false;
38
53
  *
39
54
  * @param config The configuration.
40
55
  */
41
- export function configure(config) {
56
+ export async function configure(config) {
42
57
  if (configured && !config.reset) {
43
58
  throw new ConfigError("Already configured; if you want to reset, turn on the reset flag.");
44
59
  }
60
+ await reset();
45
61
  configured = true;
46
- LoggerImpl.getLogger([]).resetDescendants();
47
62
  let metaConfigured = false;
48
63
  for (const cfg of config.loggers) {
49
64
  if (cfg.category.length === 0 ||
@@ -57,7 +72,7 @@ export function configure(config) {
57
72
  for (const sinkId of cfg.sinks ?? []) {
58
73
  const sink = config.sinks[sinkId];
59
74
  if (!sink) {
60
- reset();
75
+ await reset();
61
76
  throw new ConfigError(`Sink not found: ${sinkId}.`);
62
77
  }
63
78
  logger.sinks.push(sink);
@@ -67,12 +82,34 @@ export function configure(config) {
67
82
  for (const filterId of cfg.filters ?? []) {
68
83
  const filter = config.filters[filterId];
69
84
  if (filter === undefined) {
70
- reset();
85
+ await reset();
71
86
  throw new ConfigError(`Filter not found: ${filterId}.`);
72
87
  }
73
88
  logger.filters.push(toFilter(filter));
74
89
  }
75
90
  }
91
+ for (const sink of Object.values(config.sinks)) {
92
+ if (Symbol.asyncDispose in sink) {
93
+ asyncDisposables.add(sink);
94
+ }
95
+ if (Symbol.dispose in sink)
96
+ disposables.add(sink);
97
+ }
98
+ for (const filter of Object.values(config.filters)) {
99
+ if (filter == null || typeof filter === "string")
100
+ continue;
101
+ if (Symbol.asyncDispose in filter) {
102
+ asyncDisposables.add(filter);
103
+ }
104
+ if (Symbol.dispose in filter)
105
+ disposables.add(filter);
106
+ }
107
+ if ("process" in dntShim.dntGlobalThis) { // @ts-ignore: It's fine to use process in Node
108
+ process.on("exit", dispose);
109
+ }
110
+ else { // @ts-ignore: It's fine to addEventListener() on the browser/Deno
111
+ addEventListener("unload", dispose);
112
+ }
76
113
  const meta = LoggerImpl.getLogger(["logtape", "meta"]);
77
114
  if (!metaConfigured) {
78
115
  meta.sinks.push(getConsoleSink());
@@ -89,10 +126,25 @@ export function configure(config) {
89
126
  /**
90
127
  * Reset the configuration. Mostly for testing purposes.
91
128
  */
92
- export function reset() {
129
+ export async function reset() {
130
+ await dispose();
93
131
  LoggerImpl.getLogger([]).resetDescendants();
94
132
  configured = false;
95
133
  }
134
+ /**
135
+ * Dispose of the disposables.
136
+ */
137
+ export async function dispose() {
138
+ for (const disposable of disposables)
139
+ disposable[Symbol.dispose]();
140
+ disposables.clear();
141
+ const promises = [];
142
+ for (const disposable of asyncDisposables) {
143
+ promises.push(disposable[Symbol.asyncDispose]());
144
+ asyncDisposables.delete(disposable);
145
+ }
146
+ await Promise.all(promises);
147
+ }
96
148
  /**
97
149
  * A configuration error.
98
150
  */
@@ -0,0 +1,32 @@
1
+ import * as dntShim from "./_dnt.shims.js";
2
+ import fs from "node:fs";
3
+ import { webDriver } from "./filesink.web.js";
4
+ import { getFileSink as getBaseFileSink, } from "./sink.js";
5
+ /**
6
+ * A Node.js-specific file sink driver.
7
+ */
8
+ export const nodeDriver = {
9
+ openSync(path) {
10
+ return fs.openSync(path, "a");
11
+ },
12
+ writeSync: fs.writeSync,
13
+ flushSync: fs.fsyncSync,
14
+ closeSync: fs.closeSync,
15
+ };
16
+ /**
17
+ * Get a file sink.
18
+ *
19
+ * Note that this function is unavailable in the browser.
20
+ *
21
+ * @param path A path to the file to write to.
22
+ * @param options The options for the sink.
23
+ * @returns A sink that writes to the file. The sink is also a disposable
24
+ * object that closes the file when disposed.
25
+ */
26
+ export function getFileSink(path, options = {}) {
27
+ if ("document" in dntShim.dntGlobalThis) {
28
+ return getBaseFileSink(path, { ...options, ...webDriver });
29
+ }
30
+ return getBaseFileSink(path, { ...options, ...nodeDriver });
31
+ }
32
+ // cSpell: ignore filesink
@@ -0,0 +1,12 @@
1
+ function notImplemented() {
2
+ throw new Error("File sink is not available in the browser.");
3
+ }
4
+ /**
5
+ * A browser-specific file sink driver. All methods throw an error.
6
+ */
7
+ export const webDriver = {
8
+ openSync: notImplemented,
9
+ writeSync: notImplemented,
10
+ flushSync: notImplemented,
11
+ closeSync: notImplemented,
12
+ };
package/esm/formatter.js CHANGED
@@ -29,7 +29,7 @@ const inspect = eval(`(
29
29
  * The default text formatter. This formatter formats log records as follows:
30
30
  *
31
31
  * ```
32
- * 2023-11-14 22:13:20.000 +00:00 [INF] Hello, world!
32
+ * 2023-11-14 22:13:20.000 +00:00 [INF] category·subcategory: Hello, world!
33
33
  * ```
34
34
  *
35
35
  * @param record The log record to format.
@@ -44,7 +44,8 @@ export function defaultTextFormatter(record) {
44
44
  else
45
45
  msg += inspect(record.message[i]);
46
46
  }
47
- return `${ts.toISOString().replace("T", " ").replace("Z", " +00:00")} [${levelAbbreviations[record.level]}] ${msg}\n`;
47
+ const category = record.category.join("\xb7");
48
+ return `${ts.toISOString().replace("T", " ").replace("Z", " +00:00")} [${levelAbbreviations[record.level]}] ${category}: ${msg}\n`;
48
49
  }
49
50
  /**
50
51
  * The styles for the log level in the console.
@@ -52,9 +53,9 @@ export function defaultTextFormatter(record) {
52
53
  const logLevelStyles = {
53
54
  "debug": "background-color: gray; color: white;",
54
55
  "info": "background-color: white; color: black;",
55
- "warning": "background-color: orange;",
56
- "error": "background-color: red;",
57
- "fatal": "background-color: maroon;",
56
+ "warning": "background-color: orange; color: black;",
57
+ "error": "background-color: red; color: white;",
58
+ "fatal": "background-color: maroon; color: white;",
58
59
  };
59
60
  /**
60
61
  * The default console formatter.
@@ -74,8 +75,11 @@ export function defaultConsoleFormatter(record) {
74
75
  values.push(record.message[i]);
75
76
  }
76
77
  }
78
+ const date = new Date(record.timestamp);
79
+ const time = `${date.getUTCHours().toString().padStart(2, "0")}:${date.getUTCMinutes().toString().padStart(2, "0")}:${date.getUTCSeconds().toString().padStart(2, "0")}.${date.getUTCMilliseconds().toString().padStart(3, "0")}`;
77
80
  return [
78
- `%c${record.level.toUpperCase()}%c %c${record.category.join("\xb7")} %c${msg}`,
81
+ `%c${time} %c${levelAbbreviations[record.level]}%c %c${record.category.join("\xb7")} %c${msg}`,
82
+ "color: gray;",
79
83
  logLevelStyles[record.level],
80
84
  "background-color: default;",
81
85
  "color: gray;",
package/esm/mod.js CHANGED
@@ -1,5 +1,7 @@
1
- export { ConfigError, configure, } from "./config.js";
1
+ export { ConfigError, configure, dispose, reset, } from "./config.js";
2
+ export { getFileSink } from "./filesink.node.js";
2
3
  export { getLevelFilter, toFilter, } from "./filter.js";
3
- export { defaultConsoleFormatter, } from "./formatter.js";
4
+ export { defaultConsoleFormatter, defaultTextFormatter, } from "./formatter.js";
4
5
  export { getLogger } from "./logger.js";
5
- export { getConsoleSink } from "./sink.js";
6
+ export { getConsoleSink, getStreamSink, } from "./sink.js";
7
+ // cSpell: ignore filesink
package/esm/sink.js CHANGED
@@ -20,28 +20,29 @@ import { defaultConsoleFormatter, defaultTextFormatter, } from "./formatter.js";
20
20
  * ```
21
21
  *
22
22
  * @param stream The stream to write to.
23
- * @param formatter The text formatter to use. Defaults to
24
- * {@link defaultTextFormatter}.
25
- * @param encoder The text encoder to use. Defaults to an instance of
26
- * {@link TextEncoder}.
23
+ * @param options The options for the sink.
27
24
  * @returns A sink that writes to the stream.
28
25
  */
29
- export function getStreamSink(stream, formatter = defaultTextFormatter, encoder = new TextEncoder()) {
26
+ export function getStreamSink(stream, options = {}) {
27
+ const formatter = options.formatter ?? defaultTextFormatter;
28
+ const encoder = options.encoder ?? new TextEncoder();
30
29
  const writer = stream.getWriter();
31
- return (record) => {
30
+ const sink = (record) => {
32
31
  const bytes = encoder.encode(formatter(record));
33
32
  writer.ready.then(() => writer.write(bytes));
34
33
  };
34
+ sink[Symbol.dispose] = () => writer.close();
35
+ return sink;
35
36
  }
36
37
  /**
37
38
  * A console sink factory that returns a sink that logs to the console.
38
39
  *
39
- * @param formatter A console formatter. Defaults to
40
- * {@link defaultConsoleFormatter}.
41
- * @param console The console to log to. Defaults to {@link console}.
40
+ * @param options The options for the sink.
42
41
  * @returns A sink that logs to the console.
43
42
  */
44
- export function getConsoleSink(formatter = defaultConsoleFormatter, console = globalThis.console) {
43
+ export function getConsoleSink(options = {}) {
44
+ const formatter = options.formatter ?? defaultConsoleFormatter;
45
+ const console = options.console ?? globalThis.console;
45
46
  return (record) => {
46
47
  const args = formatter(record);
47
48
  if (record.level === "debug")
@@ -57,3 +58,23 @@ export function getConsoleSink(formatter = defaultConsoleFormatter, console = gl
57
58
  throw new TypeError(`Invalid log level: ${record.level}.`);
58
59
  };
59
60
  }
61
+ /**
62
+ * Get a platform-independent file sink.
63
+ *
64
+ * @typeParam TFile The type of the file descriptor.
65
+ * @param path A path to the file to write to.
66
+ * @param options The options for the sink and the file driver.
67
+ * @returns A sink that writes to the file. The sink is also a disposable
68
+ * object that closes the file when disposed.
69
+ */
70
+ export function getFileSink(path, options) {
71
+ const formatter = options.formatter ?? defaultTextFormatter;
72
+ const encoder = options.encoder ?? new TextEncoder();
73
+ const fd = options.openSync(path);
74
+ const sink = (record) => {
75
+ options.writeSync(fd, encoder.encode(formatter(record)));
76
+ options.flushSync(fd);
77
+ };
78
+ sink[Symbol.dispose] = () => options.closeSync(fd);
79
+ return sink;
80
+ }
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "0.1.0-dev.9+dfa649b3",
3
+ "version": "0.2.0-dev.19+9c900230",
4
4
  "description": "Simple logging library for Deno/Node.js/Bun/browsers",
5
+ "keywords": [
6
+ "logging",
7
+ "log",
8
+ "logger"
9
+ ],
5
10
  "author": {
6
11
  "name": "Hong Minhee",
7
12
  "email": "hong@minhee.org",
package/script/config.js CHANGED
@@ -1,16 +1,54 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ConfigError = exports.reset = exports.configure = void 0;
26
+ exports.ConfigError = exports.dispose = exports.reset = exports.configure = void 0;
27
+ const dntShim = __importStar(require("./_dnt.shims.js"));
4
28
  const filter_js_1 = require("./filter.js");
5
29
  const logger_js_1 = require("./logger.js");
6
30
  const sink_js_1 = require("./sink.js");
31
+ /**
32
+ * Whether the loggers are configured.
33
+ */
7
34
  let configured = false;
35
+ /**
36
+ * Disposables to dispose when resetting the configuration.
37
+ */
38
+ const disposables = new Set();
39
+ /**
40
+ * Async disposables to dispose when resetting the configuration.
41
+ */
42
+ const asyncDisposables = new Set();
8
43
  /**
9
44
  * Configure the loggers with the specified configuration.
10
45
  *
46
+ * Note that if the given sinks or filters are disposable, they will be
47
+ * disposed when the configuration is reset, or when the process exits.
48
+ *
11
49
  * @example
12
50
  * ```typescript
13
- * configure({
51
+ * await configure({
14
52
  * sinks: {
15
53
  * console: getConsoleSink(),
16
54
  * },
@@ -41,12 +79,12 @@ let configured = false;
41
79
  *
42
80
  * @param config The configuration.
43
81
  */
44
- function configure(config) {
82
+ async function configure(config) {
45
83
  if (configured && !config.reset) {
46
84
  throw new ConfigError("Already configured; if you want to reset, turn on the reset flag.");
47
85
  }
86
+ await reset();
48
87
  configured = true;
49
- logger_js_1.LoggerImpl.getLogger([]).resetDescendants();
50
88
  let metaConfigured = false;
51
89
  for (const cfg of config.loggers) {
52
90
  if (cfg.category.length === 0 ||
@@ -60,7 +98,7 @@ function configure(config) {
60
98
  for (const sinkId of cfg.sinks ?? []) {
61
99
  const sink = config.sinks[sinkId];
62
100
  if (!sink) {
63
- reset();
101
+ await reset();
64
102
  throw new ConfigError(`Sink not found: ${sinkId}.`);
65
103
  }
66
104
  logger.sinks.push(sink);
@@ -70,12 +108,34 @@ function configure(config) {
70
108
  for (const filterId of cfg.filters ?? []) {
71
109
  const filter = config.filters[filterId];
72
110
  if (filter === undefined) {
73
- reset();
111
+ await reset();
74
112
  throw new ConfigError(`Filter not found: ${filterId}.`);
75
113
  }
76
114
  logger.filters.push((0, filter_js_1.toFilter)(filter));
77
115
  }
78
116
  }
117
+ for (const sink of Object.values(config.sinks)) {
118
+ if (Symbol.asyncDispose in sink) {
119
+ asyncDisposables.add(sink);
120
+ }
121
+ if (Symbol.dispose in sink)
122
+ disposables.add(sink);
123
+ }
124
+ for (const filter of Object.values(config.filters)) {
125
+ if (filter == null || typeof filter === "string")
126
+ continue;
127
+ if (Symbol.asyncDispose in filter) {
128
+ asyncDisposables.add(filter);
129
+ }
130
+ if (Symbol.dispose in filter)
131
+ disposables.add(filter);
132
+ }
133
+ if ("process" in dntShim.dntGlobalThis) { // @ts-ignore: It's fine to use process in Node
134
+ process.on("exit", dispose);
135
+ }
136
+ else { // @ts-ignore: It's fine to addEventListener() on the browser/Deno
137
+ addEventListener("unload", dispose);
138
+ }
79
139
  const meta = logger_js_1.LoggerImpl.getLogger(["logtape", "meta"]);
80
140
  if (!metaConfigured) {
81
141
  meta.sinks.push((0, sink_js_1.getConsoleSink)());
@@ -93,11 +153,27 @@ exports.configure = configure;
93
153
  /**
94
154
  * Reset the configuration. Mostly for testing purposes.
95
155
  */
96
- function reset() {
156
+ async function reset() {
157
+ await dispose();
97
158
  logger_js_1.LoggerImpl.getLogger([]).resetDescendants();
98
159
  configured = false;
99
160
  }
100
161
  exports.reset = reset;
162
+ /**
163
+ * Dispose of the disposables.
164
+ */
165
+ async function dispose() {
166
+ for (const disposable of disposables)
167
+ disposable[Symbol.dispose]();
168
+ disposables.clear();
169
+ const promises = [];
170
+ for (const disposable of asyncDisposables) {
171
+ promises.push(disposable[Symbol.asyncDispose]());
172
+ asyncDisposables.delete(disposable);
173
+ }
174
+ await Promise.all(promises);
175
+ }
176
+ exports.dispose = dispose;
101
177
  /**
102
178
  * A configuration error.
103
179
  */
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.getFileSink = exports.nodeDriver = void 0;
30
+ const dntShim = __importStar(require("./_dnt.shims.js"));
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const filesink_web_js_1 = require("./filesink.web.js");
33
+ const sink_js_1 = require("./sink.js");
34
+ /**
35
+ * A Node.js-specific file sink driver.
36
+ */
37
+ exports.nodeDriver = {
38
+ openSync(path) {
39
+ return node_fs_1.default.openSync(path, "a");
40
+ },
41
+ writeSync: node_fs_1.default.writeSync,
42
+ flushSync: node_fs_1.default.fsyncSync,
43
+ closeSync: node_fs_1.default.closeSync,
44
+ };
45
+ /**
46
+ * Get a file sink.
47
+ *
48
+ * Note that this function is unavailable in the browser.
49
+ *
50
+ * @param path A path to the file to write to.
51
+ * @param options The options for the sink.
52
+ * @returns A sink that writes to the file. The sink is also a disposable
53
+ * object that closes the file when disposed.
54
+ */
55
+ function getFileSink(path, options = {}) {
56
+ if ("document" in dntShim.dntGlobalThis) {
57
+ return (0, sink_js_1.getFileSink)(path, { ...options, ...filesink_web_js_1.webDriver });
58
+ }
59
+ return (0, sink_js_1.getFileSink)(path, { ...options, ...exports.nodeDriver });
60
+ }
61
+ exports.getFileSink = getFileSink;
62
+ // cSpell: ignore filesink
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webDriver = void 0;
4
+ function notImplemented() {
5
+ throw new Error("File sink is not available in the browser.");
6
+ }
7
+ /**
8
+ * A browser-specific file sink driver. All methods throw an error.
9
+ */
10
+ exports.webDriver = {
11
+ openSync: notImplemented,
12
+ writeSync: notImplemented,
13
+ flushSync: notImplemented,
14
+ closeSync: notImplemented,
15
+ };
@@ -32,7 +32,7 @@ const inspect = eval(`(
32
32
  * The default text formatter. This formatter formats log records as follows:
33
33
  *
34
34
  * ```
35
- * 2023-11-14 22:13:20.000 +00:00 [INF] Hello, world!
35
+ * 2023-11-14 22:13:20.000 +00:00 [INF] category·subcategory: Hello, world!
36
36
  * ```
37
37
  *
38
38
  * @param record The log record to format.
@@ -47,7 +47,8 @@ function defaultTextFormatter(record) {
47
47
  else
48
48
  msg += inspect(record.message[i]);
49
49
  }
50
- return `${ts.toISOString().replace("T", " ").replace("Z", " +00:00")} [${levelAbbreviations[record.level]}] ${msg}\n`;
50
+ const category = record.category.join("\xb7");
51
+ return `${ts.toISOString().replace("T", " ").replace("Z", " +00:00")} [${levelAbbreviations[record.level]}] ${category}: ${msg}\n`;
51
52
  }
52
53
  exports.defaultTextFormatter = defaultTextFormatter;
53
54
  /**
@@ -56,9 +57,9 @@ exports.defaultTextFormatter = defaultTextFormatter;
56
57
  const logLevelStyles = {
57
58
  "debug": "background-color: gray; color: white;",
58
59
  "info": "background-color: white; color: black;",
59
- "warning": "background-color: orange;",
60
- "error": "background-color: red;",
61
- "fatal": "background-color: maroon;",
60
+ "warning": "background-color: orange; color: black;",
61
+ "error": "background-color: red; color: white;",
62
+ "fatal": "background-color: maroon; color: white;",
62
63
  };
63
64
  /**
64
65
  * The default console formatter.
@@ -78,8 +79,11 @@ function defaultConsoleFormatter(record) {
78
79
  values.push(record.message[i]);
79
80
  }
80
81
  }
82
+ const date = new Date(record.timestamp);
83
+ const time = `${date.getUTCHours().toString().padStart(2, "0")}:${date.getUTCMinutes().toString().padStart(2, "0")}:${date.getUTCSeconds().toString().padStart(2, "0")}.${date.getUTCMilliseconds().toString().padStart(3, "0")}`;
81
84
  return [
82
- `%c${record.level.toUpperCase()}%c %c${record.category.join("\xb7")} %c${msg}`,
85
+ `%c${time} %c${levelAbbreviations[record.level]}%c %c${record.category.join("\xb7")} %c${msg}`,
86
+ "color: gray;",
83
87
  logLevelStyles[record.level],
84
88
  "background-color: default;",
85
89
  "color: gray;",
package/script/mod.js CHANGED
@@ -1,15 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getConsoleSink = exports.getLogger = exports.defaultConsoleFormatter = exports.toFilter = exports.getLevelFilter = exports.configure = exports.ConfigError = void 0;
3
+ exports.getStreamSink = exports.getConsoleSink = exports.getLogger = exports.defaultTextFormatter = exports.defaultConsoleFormatter = exports.toFilter = exports.getLevelFilter = exports.getFileSink = exports.reset = exports.dispose = exports.configure = exports.ConfigError = void 0;
4
4
  var config_js_1 = require("./config.js");
5
5
  Object.defineProperty(exports, "ConfigError", { enumerable: true, get: function () { return config_js_1.ConfigError; } });
6
6
  Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return config_js_1.configure; } });
7
+ Object.defineProperty(exports, "dispose", { enumerable: true, get: function () { return config_js_1.dispose; } });
8
+ Object.defineProperty(exports, "reset", { enumerable: true, get: function () { return config_js_1.reset; } });
9
+ var filesink_node_js_1 = require("./filesink.node.js");
10
+ Object.defineProperty(exports, "getFileSink", { enumerable: true, get: function () { return filesink_node_js_1.getFileSink; } });
7
11
  var filter_js_1 = require("./filter.js");
8
12
  Object.defineProperty(exports, "getLevelFilter", { enumerable: true, get: function () { return filter_js_1.getLevelFilter; } });
9
13
  Object.defineProperty(exports, "toFilter", { enumerable: true, get: function () { return filter_js_1.toFilter; } });
10
14
  var formatter_js_1 = require("./formatter.js");
11
15
  Object.defineProperty(exports, "defaultConsoleFormatter", { enumerable: true, get: function () { return formatter_js_1.defaultConsoleFormatter; } });
16
+ Object.defineProperty(exports, "defaultTextFormatter", { enumerable: true, get: function () { return formatter_js_1.defaultTextFormatter; } });
12
17
  var logger_js_1 = require("./logger.js");
13
18
  Object.defineProperty(exports, "getLogger", { enumerable: true, get: function () { return logger_js_1.getLogger; } });
14
19
  var sink_js_1 = require("./sink.js");
15
20
  Object.defineProperty(exports, "getConsoleSink", { enumerable: true, get: function () { return sink_js_1.getConsoleSink; } });
21
+ Object.defineProperty(exports, "getStreamSink", { enumerable: true, get: function () { return sink_js_1.getStreamSink; } });
22
+ // cSpell: ignore filesink
package/script/sink.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getConsoleSink = exports.getStreamSink = void 0;
3
+ exports.getFileSink = exports.getConsoleSink = exports.getStreamSink = void 0;
4
4
  const formatter_js_1 = require("./formatter.js");
5
5
  /**
6
6
  * A factory that returns a sink that writes to a {@link WritableStream}.
@@ -23,29 +23,30 @@ const formatter_js_1 = require("./formatter.js");
23
23
  * ```
24
24
  *
25
25
  * @param stream The stream to write to.
26
- * @param formatter The text formatter to use. Defaults to
27
- * {@link defaultTextFormatter}.
28
- * @param encoder The text encoder to use. Defaults to an instance of
29
- * {@link TextEncoder}.
26
+ * @param options The options for the sink.
30
27
  * @returns A sink that writes to the stream.
31
28
  */
32
- function getStreamSink(stream, formatter = formatter_js_1.defaultTextFormatter, encoder = new TextEncoder()) {
29
+ function getStreamSink(stream, options = {}) {
30
+ const formatter = options.formatter ?? formatter_js_1.defaultTextFormatter;
31
+ const encoder = options.encoder ?? new TextEncoder();
33
32
  const writer = stream.getWriter();
34
- return (record) => {
33
+ const sink = (record) => {
35
34
  const bytes = encoder.encode(formatter(record));
36
35
  writer.ready.then(() => writer.write(bytes));
37
36
  };
37
+ sink[Symbol.dispose] = () => writer.close();
38
+ return sink;
38
39
  }
39
40
  exports.getStreamSink = getStreamSink;
40
41
  /**
41
42
  * A console sink factory that returns a sink that logs to the console.
42
43
  *
43
- * @param formatter A console formatter. Defaults to
44
- * {@link defaultConsoleFormatter}.
45
- * @param console The console to log to. Defaults to {@link console}.
44
+ * @param options The options for the sink.
46
45
  * @returns A sink that logs to the console.
47
46
  */
48
- function getConsoleSink(formatter = formatter_js_1.defaultConsoleFormatter, console = globalThis.console) {
47
+ function getConsoleSink(options = {}) {
48
+ const formatter = options.formatter ?? formatter_js_1.defaultConsoleFormatter;
49
+ const console = options.console ?? globalThis.console;
49
50
  return (record) => {
50
51
  const args = formatter(record);
51
52
  if (record.level === "debug")
@@ -62,3 +63,24 @@ function getConsoleSink(formatter = formatter_js_1.defaultConsoleFormatter, cons
62
63
  };
63
64
  }
64
65
  exports.getConsoleSink = getConsoleSink;
66
+ /**
67
+ * Get a platform-independent file sink.
68
+ *
69
+ * @typeParam TFile The type of the file descriptor.
70
+ * @param path A path to the file to write to.
71
+ * @param options The options for the sink and the file driver.
72
+ * @returns A sink that writes to the file. The sink is also a disposable
73
+ * object that closes the file when disposed.
74
+ */
75
+ function getFileSink(path, options) {
76
+ const formatter = options.formatter ?? formatter_js_1.defaultTextFormatter;
77
+ const encoder = options.encoder ?? new TextEncoder();
78
+ const fd = options.openSync(path);
79
+ const sink = (record) => {
80
+ options.writeSync(fd, encoder.encode(formatter(record)));
81
+ options.flushSync(fd);
82
+ };
83
+ sink[Symbol.dispose] = () => options.closeSync(fd);
84
+ return sink;
85
+ }
86
+ exports.getFileSink = getFileSink;
package/types/config.d.ts CHANGED
@@ -50,9 +50,12 @@ export interface LoggerConfig<TSinkId extends string, TFilterId extends string>
50
50
  /**
51
51
  * Configure the loggers with the specified configuration.
52
52
  *
53
+ * Note that if the given sinks or filters are disposable, they will be
54
+ * disposed when the configuration is reset, or when the process exits.
55
+ *
53
56
  * @example
54
57
  * ```typescript
55
- * configure({
58
+ * await configure({
56
59
  * sinks: {
57
60
  * console: getConsoleSink(),
58
61
  * },
@@ -83,11 +86,15 @@ export interface LoggerConfig<TSinkId extends string, TFilterId extends string>
83
86
  *
84
87
  * @param config The configuration.
85
88
  */
86
- export declare function configure<TSinkId extends string, TFilterId extends string>(config: Config<TSinkId, TFilterId>): void;
89
+ export declare function configure<TSinkId extends string, TFilterId extends string>(config: Config<TSinkId, TFilterId>): Promise<void>;
87
90
  /**
88
91
  * Reset the configuration. Mostly for testing purposes.
89
92
  */
90
- export declare function reset(): void;
93
+ export declare function reset(): Promise<void>;
94
+ /**
95
+ * Dispose of the disposables.
96
+ */
97
+ export declare function dispose(): Promise<void>;
91
98
  /**
92
99
  * A configuration error.
93
100
  */
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAkB,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM;IACtE;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAEvC;;OAEG;IACH,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IAE5C;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAC3B,OAAO,SAAS,MAAM,EACtB,SAAS,SAAS,MAAM;IAExB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAElB;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;IAEtB;;;OAGG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CACzB;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM,EACxE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,QA0DnC;AAED;;GAEG;AACH,wBAAgB,KAAK,SAGpB;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC;;;OAGG;gBACS,OAAO,EAAE,MAAM;CAI5B"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAkB,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM;IACtE;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAEvC;;OAEG;IACH,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IAE5C;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAC3B,OAAO,SAAS,MAAM,EACtB,SAAS,SAAS,MAAM;IAExB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE5B;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAElB;;OAEG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;IAEtB;;;OAGG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CACzB;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAsB,SAAS,CAC7B,OAAO,SAAS,MAAM,EACtB,SAAS,SAAS,MAAM,EACxB,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA8EnD;AAED;;GAEG;AACH,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAI3C;AAED;;GAEG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAS7C;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC;;;OAGG;gBACS,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../../../src/deps/deno.land/x/which_runtime@0.2.0/mod.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,MAAM,SAAgC,CAAC;AACpD,eAAO,MAAM,MAAM,SAA0B,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert_rejects.d.ts","sourceRoot":"","sources":["../../../../../../src/deps/jsr.io/@std/assert/0.222.1/assert_rejects.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,EAC9B,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAAC;AACpB;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,EACnD,EAAE,EAAE,MAAM,WAAW,CAAC,OAAO,CAAC,EAE9B,UAAU,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EACrC,WAAW,CAAC,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ /// <reference types="node" />
2
+ import { type FileSinkDriver, type FileSinkOptions, type Sink } from "./sink.js";
3
+ /**
4
+ * A Node.js-specific file sink driver.
5
+ */
6
+ export declare const nodeDriver: FileSinkDriver<number>;
7
+ /**
8
+ * Get a file sink.
9
+ *
10
+ * Note that this function is unavailable in the browser.
11
+ *
12
+ * @param path A path to the file to write to.
13
+ * @param options The options for the sink.
14
+ * @returns A sink that writes to the file. The sink is also a disposable
15
+ * object that closes the file when disposed.
16
+ */
17
+ export declare function getFileSink(path: string, options?: FileSinkOptions): Sink & Disposable;
18
+ //# sourceMappingURL=filesink.node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesink.node.d.ts","sourceRoot":"","sources":["../src/filesink.node.ts"],"names":[],"mappings":";AAGA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,eAAe,EAEpB,KAAK,IAAI,EACV,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,eAAO,MAAM,UAAU,EAAE,cAAc,CAAC,MAAM,CAO7C,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAoB,GAC5B,IAAI,GAAG,UAAU,CAKnB"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesink.test.d.ts","sourceRoot":"","sources":["../src/filesink.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ import type { FileSinkDriver } from "./sink.js";
2
+ /**
3
+ * A browser-specific file sink driver. All methods throw an error.
4
+ */
5
+ export declare const webDriver: FileSinkDriver<void>;
6
+ //# sourceMappingURL=filesink.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesink.web.d.ts","sourceRoot":"","sources":["../src/filesink.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAMhD;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,cAAc,CAAC,IAAI,CAK1C,CAAC"}
@@ -11,7 +11,7 @@ export type TextFormatter = (record: LogRecord) => string;
11
11
  * The default text formatter. This formatter formats log records as follows:
12
12
  *
13
13
  * ```
14
- * 2023-11-14 22:13:20.000 +00:00 [INF] Hello, world!
14
+ * 2023-11-14 22:13:20.000 +00:00 [INF] category·subcategory: Hello, world!
15
15
  * ```
16
16
  *
17
17
  * @param record The log record to format.
@@ -1 +1 @@
1
- {"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../src/formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,MAAM,CAAC;AA+B1D;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAU9D;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,SAAS,OAAO,EAAE,CAAC;AAazE;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,OAAO,EAAE,CAoB7E"}
1
+ {"version":3,"file":"formatter.d.ts","sourceRoot":"","sources":["../src/formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,MAAM,CAAC;AA+B1D;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAW9D;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,SAAS,OAAO,EAAE,CAAC;AAazE;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,OAAO,EAAE,CA2B7E"}
package/types/mod.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- export { type Config, ConfigError, configure, type LoggerConfig, } from "./config.js";
1
+ export { type Config, ConfigError, configure, dispose, type LoggerConfig, reset, } from "./config.js";
2
+ export { getFileSink } from "./filesink.node.js";
2
3
  export { type Filter, type FilterLike, getLevelFilter, toFilter, } from "./filter.js";
3
- export { type ConsoleFormatter, defaultConsoleFormatter, type TextFormatter, } from "./formatter.js";
4
+ export { type ConsoleFormatter, defaultConsoleFormatter, defaultTextFormatter, type TextFormatter, } from "./formatter.js";
4
5
  export { getLogger, type Logger } from "./logger.js";
5
6
  export type { LogLevel, LogRecord } from "./record.js";
6
- export { getConsoleSink, type Sink } from "./sink.js";
7
+ export { type ConsoleSinkOptions, type FileSinkDriver, type FileSinkOptions, getConsoleSink, getStreamSink, type Sink, type StreamSinkOptions, } from "./sink.js";
7
8
  //# sourceMappingURL=mod.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EACX,WAAW,EACX,SAAS,EACT,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,cAAc,EACd,QAAQ,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,gBAAgB,EACrB,uBAAuB,EACvB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EACX,WAAW,EACX,SAAS,EACT,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,GACN,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,cAAc,EACd,QAAQ,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,gBAAgB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,cAAc,EACd,aAAa,EACb,KAAK,IAAI,EACT,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC"}
package/types/sink.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
+ /// <reference types="node" />
3
4
  import * as dntShim from "./_dnt.shims.js";
4
5
  import { type ConsoleFormatter, type TextFormatter } from "./formatter.js";
5
6
  import type { LogRecord } from "./record.js";
@@ -13,6 +14,21 @@ import type { LogRecord } from "./record.js";
13
14
  * @param record The log record to sink.
14
15
  */
15
16
  export type Sink = (record: LogRecord) => void;
17
+ /**
18
+ * Options for the {@link getStreamSink} function.
19
+ */
20
+ export interface StreamSinkOptions {
21
+ /**
22
+ * The text formatter to use. Defaults to {@link defaultTextFormatter}.
23
+ */
24
+ formatter?: TextFormatter;
25
+ /**
26
+ * The text encoder to use. Defaults to an instance of {@link TextEncoder}.
27
+ */
28
+ encoder?: {
29
+ encode(text: string): Uint8Array;
30
+ };
31
+ }
16
32
  /**
17
33
  * A factory that returns a sink that writes to a {@link WritableStream}.
18
34
  *
@@ -34,22 +50,69 @@ export type Sink = (record: LogRecord) => void;
34
50
  * ```
35
51
  *
36
52
  * @param stream The stream to write to.
37
- * @param formatter The text formatter to use. Defaults to
38
- * {@link defaultTextFormatter}.
39
- * @param encoder The text encoder to use. Defaults to an instance of
40
- * {@link TextEncoder}.
53
+ * @param options The options for the sink.
41
54
  * @returns A sink that writes to the stream.
42
55
  */
43
- export declare function getStreamSink(stream: dntShim.WritableStream, formatter?: TextFormatter, encoder?: {
44
- encode(text: string): Uint8Array;
45
- }): Sink;
56
+ export declare function getStreamSink(stream: dntShim.WritableStream, options?: StreamSinkOptions): Sink & Disposable;
57
+ /**
58
+ * Options for the {@link getConsoleSink} function.
59
+ */
60
+ export interface ConsoleSinkOptions {
61
+ /**
62
+ * The console formatter to use. Defaults to {@link defaultConsoleFormatter}.
63
+ */
64
+ formatter?: ConsoleFormatter;
65
+ /**
66
+ * The console to log to. Defaults to {@link console}.
67
+ */
68
+ console?: Console;
69
+ }
46
70
  /**
47
71
  * A console sink factory that returns a sink that logs to the console.
48
72
  *
49
- * @param formatter A console formatter. Defaults to
50
- * {@link defaultConsoleFormatter}.
51
- * @param console The console to log to. Defaults to {@link console}.
73
+ * @param options The options for the sink.
52
74
  * @returns A sink that logs to the console.
53
75
  */
54
- export declare function getConsoleSink(formatter?: ConsoleFormatter, console?: Console): Sink;
76
+ export declare function getConsoleSink(options?: ConsoleSinkOptions): Sink;
77
+ /**
78
+ * Options for the {@link getFileSink} function.
79
+ */
80
+ export type FileSinkOptions = StreamSinkOptions;
81
+ /**
82
+ * A platform-specific file sink driver.
83
+ * @typeParam TFile The type of the file descriptor.
84
+ */
85
+ export interface FileSinkDriver<TFile> {
86
+ /**
87
+ * Open a file for appending and return a file descriptor.
88
+ * @param path A path to the file to open.
89
+ */
90
+ openSync(path: string): TFile;
91
+ /**
92
+ * Write a chunk of data to the file.
93
+ * @param fd The file descriptor.
94
+ * @param chunk The data to write.
95
+ */
96
+ writeSync(fd: TFile, chunk: Uint8Array): void;
97
+ /**
98
+ * Flush the file to ensure that all data is written to the disk.
99
+ * @param fd The file descriptor.
100
+ */
101
+ flushSync(fd: TFile): void;
102
+ /**
103
+ * Close the file.
104
+ * @param fd The file descriptor.
105
+ */
106
+ closeSync(fd: TFile): void;
107
+ }
108
+ /**
109
+ * Get a platform-independent file sink.
110
+ *
111
+ * @typeParam TFile The type of the file descriptor.
112
+ * @param path A path to the file to write to.
113
+ * @param options The options for the sink and the file driver.
114
+ * @returns A sink that writes to the file. The sink is also a disposable
115
+ * object that closes the file when disposed.
116
+ */
117
+ export declare function getFileSink<TFile>(path: string, options: FileSinkOptions & FileSinkDriver<TFile>): Sink & Disposable;
55
118
  //# sourceMappingURL=sink.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../src/sink.ts"],"names":[],"mappings":";;AAAA,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,CAAC,cAAc,EAC9B,SAAS,GAAE,aAAoC,EAC/C,OAAO,GAAE;IAAE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;CAAsB,GAChE,IAAI,CAMN;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,SAAS,GAAE,gBAA0C,EACrD,OAAO,GAAE,OAA4B,GACpC,IAAI,CAUN"}
1
+ {"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../src/sink.ts"],"names":[],"mappings":";;;AAAA,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;IAE1B;;OAEG;IACH,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,CAAC,cAAc,EAC9B,OAAO,GAAE,iBAAsB,GAC9B,IAAI,GAAG,UAAU,CAUnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAE7B;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,kBAAuB,GAAG,IAAI,CAYrE;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEhD;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK;IACnC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;IAE9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAE9C;;;OAGG;IACH,SAAS,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC,GAC/C,IAAI,GAAG,UAAU,CAUnB"}