Skip to content

Commit

Permalink
File sink
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Apr 18, 2024
1 parent 11d80a5 commit 777b023
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 5 deletions.
67 changes: 65 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,68 @@ in the API reference for more details.
[`getStreamSink()`]: https://jsr.io/@logtape/logtape/doc/~/getStreamSink
[`StreamSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/StreamSinkOptions

### File sink

LogTape provides a platform-independent file sink. You can use it by providing
a platform-specific file driver for Deno or Node.js. Here's an example of
a file sink that writes log messages to a file:

~~~~ typescript
// Deno
import { type FileSinkDriver, getFileSink } from "@logtape/logtape";

const driver: FileSinkDriver<Deno.FsFile> = {
openSync(path: string) {
return Deno.openSync(path, { create: true, append: true });
},
writeSync(fd, chunk) {
fd.writeSync(chunk);
},
flushSync(fd) {
fd.syncSync();
},
closeSync(fd) {
fd.close();
},
};

configure({
sinks: {
file: getFileSink("my-app.log", driver),
},
// Omitted for brevity
});
~~~~

~~~~ typescript
// Node.js or Bun
import fs from "node:fs";
import { type FileSinkDriver, getFileSink } from "@logtape/logtape";

const driver: FileSinkDriver<number> = {
openSync(path: string) {
return fs.openSync(path, "a");
},
writeSync: fs.writeSync,
flushSync: fs.fsyncSync,
closeSync: fs.closeSync,
};

configure({
sinks: {
file: getFileSink("my-app.log", driver),
},
// Omitted for brevity
});
~~~~

See also [`getFileSink()`] function, [`FileSinkOptions`] interface, and
[`FileSinkDriver`] interface in the API reference for more details.

[`getFileSink()`]: https://jsr.io/@logtape/logtape/doc/~/getFileSink
[`FileSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/FileSinkOptions
[`FileSinkDriver`]: https://jsr.io/@logtape/logtape/doc/~/FileSinkDriver

### Buffer sink

For testing purposes, you may want to collect log messages in memory. Although
Expand All @@ -259,8 +321,9 @@ configure({

### Text formatter

A stream sink writes log messages in a plain text format. You can customize
the format by providing a text formatter. The type of a text formatter is:
A stream sink and a file sink write log messages in a plain text format.
You can customize the format by providing a text formatter. The type of a
text formatter is:

~~~~ typescript
export type TextFormatter = (record: LogRecord) => string;
Expand Down
8 changes: 6 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
"@std/async": "jsr:@std/async@^0.222.1",
"@std/fs": "jsr:@std/fs@^0.223.0",
"@std/testing": "jsr:@std/testing@^0.222.1",
"consolemock": "npm:consolemock@^1.1.0"
"consolemock": "npm:consolemock@^1.1.0",
"which_runtime": "https://deno.land/x/which_runtime@0.2.0/mod.ts"
},
"exclude": [
"*-venv/",
"coverage/",
"npm/"
],
"unstable": [
"fs"
],
"lock": false,
"tasks": {
"check": "deno check **/*.ts && deno lint && deno fmt --check",
"test": "deno test --allow-read=logtape/__snapshots__ --allow-write=logtape/__snapshots__",
"test": "deno test --allow-read --allow-write",
"coverage": "rm -rf coverage && deno task test --coverage && deno coverage --html coverage",
"dnt": "deno run -A dnt.ts",
"hooks:install": "deno run --allow-read=deno.json,.git/hooks/ --allow-write=.git/hooks/ jsr:@hongminhee/deno-task-hooks",
Expand Down
58 changes: 57 additions & 1 deletion logtape/sink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import { assertEquals } from "@std/assert/assert-equals";
import { assertThrows } from "@std/assert/assert-throws";
import { delay } from "@std/async/delay";
import makeConsoleMock from "consolemock";
import fs from "node:fs";
import { isDeno } from "which_runtime";
import { debug, error, fatal, info, warning } from "./fixtures.ts";
import type { LogLevel } from "./record.ts";
import { getConsoleSink, getStreamSink } from "./sink.ts";
import {
type FileSinkDriver,
getConsoleSink,
getFileSink,
getStreamSink,
type Sink,
} from "./sink.ts";

interface ConsoleMock extends Console {
history(): unknown[];
Expand Down Expand Up @@ -117,3 +125,51 @@ Deno.test("getConsoleSink()", () => {
"Invalid log level: invalid.",
);
});

Deno.test("getFileSink()", () => {
const path = Deno.makeTempFileSync();
let sink: Sink & { close(): void };
if (isDeno) {
const driver: FileSinkDriver<Deno.FsFile> = {
openSync(path: string) {
return Deno.openSync(path, { create: true, append: true });
},
writeSync(fd, chunk) {
fd.writeSync(chunk);
},
flushSync(fd) {
fd.syncSync();
},
closeSync(fd) {
fd.close();
},
};
sink = getFileSink(path, driver);
} else {
const driver: FileSinkDriver<number> = {
openSync(path: string) {
return fs.openSync(path, "a");
},
writeSync: fs.writeSync,
flushSync: fs.fsyncSync,
closeSync: fs.closeSync,
};
sink = getFileSink(path, driver);
}
sink(debug);
sink(info);
sink(warning);
sink(error);
sink(fatal);
sink.close();
assertEquals(
Deno.readTextFileSync(path),
`\
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
`,
);
});
58 changes: 58 additions & 0 deletions logtape/sink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,61 @@ export function getConsoleSink(options: ConsoleSinkOptions = {}): Sink {
} else throw new TypeError(`Invalid log level: ${record.level}.`);
};
}

/**
* Options for the {@link getFileSink} function.
*/
export type FileSinkOptions = StreamSinkOptions;

/**
* A platform-specific file sink driver.
* @typeParam TFile The type of the file descriptor.
*/
export interface FileSinkDriver<TFile> {
/**
* Open a file for appending and return a file descriptor.
* @param path A path to the file to open.
*/
openSync(path: string): TFile;

/**
* Write a chunk of data to the file.
* @param fd The file descriptor.
* @param chunk The data to write.
*/
writeSync(fd: TFile, chunk: Uint8Array): void;

/**
* Flush the file to ensure that all data is written to the disk.
* @param fd The file descriptor.
*/
flushSync(fd: TFile): void;

/**
* Close the file.
* @param fd The file descriptor.
*/
closeSync(fd: TFile): void;
}

/**
* Get a platform-independent file sink.
* @typeParam TFile The type of the file descriptor.
* @param path A path to the file to write to.
* @param options The options for the sink and the file driver.
* @returns A sink that writes to the file.
*/
export function getFileSink<TFile>(
path: string,
options: FileSinkOptions & FileSinkDriver<TFile>,
): Sink & { close: () => void } {
const formatter = options.formatter ?? defaultTextFormatter;
const encoder = options.encoder ?? new TextEncoder();
const fd = options.openSync(path);
const sink = (record: LogRecord) => {
options.writeSync(fd, encoder.encode(formatter(record)));
options.flushSync(fd);
};
sink.close = () => options.closeSync(fd);
return sink;
}

0 comments on commit 777b023

Please sign in to comment.