mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
feat(std/log): improvements and new log handler (#4674)
This commit is contained in:
parent
198a045dbe
commit
475a47cfb7
6 changed files with 447 additions and 70 deletions
|
@ -5,22 +5,23 @@
|
||||||
```ts
|
```ts
|
||||||
import * as log from "https://deno.land/std/log/mod.ts";
|
import * as log from "https://deno.land/std/log/mod.ts";
|
||||||
|
|
||||||
// simple default logger, you can customize it
|
// Simple default logger out of the box. You can customize it
|
||||||
// by overriding logger and handler named "default"
|
// by overriding logger and handler named "default", or providing
|
||||||
|
// additional logger configurations
|
||||||
log.debug("Hello world");
|
log.debug("Hello world");
|
||||||
log.info("Hello world");
|
log.info("Hello world");
|
||||||
log.warning("Hello world");
|
log.warning("Hello world");
|
||||||
log.error("Hello world");
|
log.error("Hello world");
|
||||||
log.critical("500 Internal server error");
|
log.critical("500 Internal server error");
|
||||||
|
|
||||||
// custom configuration
|
// custom configuration with 2 loggers (the default and `tasks` loggers)
|
||||||
await log.setup({
|
await log.setup({
|
||||||
handlers: {
|
handlers: {
|
||||||
console: new log.handlers.ConsoleHandler("DEBUG"),
|
console: new log.handlers.ConsoleHandler("DEBUG"),
|
||||||
|
|
||||||
file: new log.handlers.FileHandler("WARNING", {
|
file: new log.handlers.FileHandler("WARNING", {
|
||||||
filename: "./log.txt",
|
filename: "./log.txt",
|
||||||
// you can change format of output message
|
// you can change format of output message using any keys in `LogRecord`
|
||||||
formatter: "{levelName} {msg}",
|
formatter: "{levelName} {msg}",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -48,7 +49,7 @@ logger.warning("buzz"); // logs to both `console` and `file` handlers
|
||||||
|
|
||||||
// get custom logger
|
// get custom logger
|
||||||
logger = log.getLogger("tasks");
|
logger = log.getLogger("tasks");
|
||||||
logger.debug("fizz"); // won't get output becase this logger has "ERROR" level
|
logger.debug("fizz"); // won't get output because this logger has "ERROR" level
|
||||||
logger.error("buzz"); // log to `console`
|
logger.error("buzz"); // log to `console`
|
||||||
|
|
||||||
// if you try to use a logger that hasn't been configured
|
// if you try to use a logger that hasn't been configured
|
||||||
|
@ -62,9 +63,9 @@ unknownLogger.info("foobar"); // no-op
|
||||||
|
|
||||||
### Loggers
|
### Loggers
|
||||||
|
|
||||||
Loggers are objects that you interact with. When you use logger method it
|
Loggers are objects that you interact with. When you use a logger method it
|
||||||
constructs a `LogRecord` and passes it down to its handlers for output. To
|
constructs a `LogRecord` and passes it down to its handlers for output. To
|
||||||
create custom loggers speficify them in `loggers` when calling `log.setup`.
|
create custom loggers, specify them in `loggers` when calling `log.setup`.
|
||||||
|
|
||||||
#### `LogRecord`
|
#### `LogRecord`
|
||||||
|
|
||||||
|
@ -72,28 +73,107 @@ create custom loggers speficify them in `loggers` when calling `log.setup`.
|
||||||
well some meta data that can be later used when formatting a message.
|
well some meta data that can be later used when formatting a message.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface LogRecord {
|
class LogRecord {
|
||||||
msg: string;
|
readonly msg: string;
|
||||||
args: any[];
|
readonly args: any[];
|
||||||
datetime: Date;
|
readonly datetime: Date;
|
||||||
level: number;
|
readonly level: number;
|
||||||
levelName: string;
|
readonly levelName: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Handlers
|
### Handlers
|
||||||
|
|
||||||
Handlers are responsible for actual output of log messages. When handler is
|
Handlers are responsible for actual output of log messages. When a handler is
|
||||||
called by logger it firstly checks that `LogRecord`'s level is not lower than
|
called by a logger, it firstly checks that `LogRecord`'s level is not lower than
|
||||||
level of the handler. If level check passes, handlers formats log record into
|
level of the handler. If level check passes, handlers formats log record into
|
||||||
string and outputs it to target.
|
string and outputs it to target.
|
||||||
|
|
||||||
`log` module comes with two built-in handlers:
|
`log` module comes with three built-in handlers:
|
||||||
|
|
||||||
- `ConsoleHandler` - (default)
|
#### `ConsoleHandler`
|
||||||
- `FileHandler`
|
|
||||||
|
|
||||||
#### Custom message format
|
This is the default logger. It will output color coded log messages to the
|
||||||
|
console via `console.log()`. This logger takes `HandlerOptions`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type FormatterFunction = (logRecord: LogRecord) => string;
|
||||||
|
|
||||||
|
interface HandlerOptions {
|
||||||
|
formatter?: string | FormatterFunction; //see `Custom message format` below
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `FileHandler`
|
||||||
|
|
||||||
|
This handler will output to a file using an optional mode (default is `a`, e.g.
|
||||||
|
append). The file will grow indefinitely. This logger takes `FileOptions`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FileHandlerOptions {
|
||||||
|
formatter?: string | FormatterFunction; //see `Custom message format` below
|
||||||
|
filename: string;
|
||||||
|
mode?: LogMode; // 'a', 'w', 'x'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior of the log modes is as follows:
|
||||||
|
|
||||||
|
- `'a'` - Default mode. Appends new log messages to the end of an existing log
|
||||||
|
file, or create a new log file if none exists
|
||||||
|
- `'w'` - Upon creation of the handler, any existing log file will be removed
|
||||||
|
and a new one created.
|
||||||
|
- `'x'` - This will create a new log file and throw an error if one already
|
||||||
|
exists
|
||||||
|
|
||||||
|
This handler requires `--allow-write` permission on the log file.
|
||||||
|
|
||||||
|
#### `RotatingFileHandler`
|
||||||
|
|
||||||
|
This handler extends the functionality of the `FileHandler` by "rotating" the
|
||||||
|
log file when it reaches a certain size. `maxBytes` specifies the maximum size
|
||||||
|
in bytes that the log file can grow to before rolling over to a new one. If the
|
||||||
|
size of the new log message plus the current log file size exceeds `maxBytes`
|
||||||
|
then a roll over is triggered. When a roll over occurs, before the log message
|
||||||
|
is written, the log file is renamed and appended with `.1`. If a `.1` version
|
||||||
|
already existed, it would have been renamed `.2` first and so on. The maximum
|
||||||
|
number of log files to keep is specified by `maxBackupCount`. After the renames
|
||||||
|
are complete the log message is written to the original, now blank, file.
|
||||||
|
|
||||||
|
Example: Given `log.txt`, `log.txt.1`, `log.txt.2` and `log.txt.3`, a
|
||||||
|
`maxBackupCount` of 3 and a new log message which would cause `log.txt` to
|
||||||
|
exceed `maxBytes`, then `log.txt.2` would be renamed to `log.txt.3` (thereby
|
||||||
|
discarding the original contents of `log.txt.3` since 3 is the maximum number of
|
||||||
|
backups to keep), `log.txt.1` would be renamed to `log.txt.2`, `log.txt` would
|
||||||
|
be renamed to `log.txt.1` and finally `log.txt` would be created from scratch
|
||||||
|
where the new log message would be written.
|
||||||
|
|
||||||
|
Options for this handler are:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RotatingFileHandlerOptions {
|
||||||
|
maxBytes: number;
|
||||||
|
maxBackupCount: number;
|
||||||
|
formatter?: string | FormatterFunction; //see `Custom message format` below
|
||||||
|
filename: string;
|
||||||
|
mode?: LogMode; // 'a', 'w', 'x'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional notes on `mode` as described above:
|
||||||
|
|
||||||
|
- `'a'` Default mode. As above, this will pick up where the logs left off in
|
||||||
|
rotation, or create a new log file if it doesn't exist.
|
||||||
|
- `'w'` in addition to starting with a clean `filename`, this mode will also
|
||||||
|
cause any existing backups (up to `maxBackupCount`) to be deleted on setup
|
||||||
|
giving a fully clean slate.
|
||||||
|
- `'x'` requires that neither `filename`, nor any backups (up to
|
||||||
|
`maxBackupCount`), exist before setup
|
||||||
|
|
||||||
|
This handler requires both `--allow-read` and `--allow-write` permissions on the
|
||||||
|
log files.
|
||||||
|
|
||||||
|
### Custom message format
|
||||||
|
|
||||||
If you want to override default format of message you can define `formatter`
|
If you want to override default format of message you can define `formatter`
|
||||||
option for handler. It can be either simple string-based format that uses
|
option for handler. It can be either simple string-based format that uses
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
const { open } = Deno;
|
const { open, openSync, close, renameSync, statSync } = Deno;
|
||||||
type File = Deno.File;
|
type File = Deno.File;
|
||||||
type Writer = Deno.Writer;
|
type Writer = Deno.Writer;
|
||||||
import { getLevelByName, LogLevel } from "./levels.ts";
|
import { getLevelByName, LogLevel } from "./levels.ts";
|
||||||
import { LogRecord } from "./logger.ts";
|
import { LogRecord } from "./logger.ts";
|
||||||
import { red, yellow, blue, bold } from "../fmt/colors.ts";
|
import { red, yellow, blue, bold } from "../fmt/colors.ts";
|
||||||
|
import { existsSync, exists } from "../fs/exists.ts";
|
||||||
|
|
||||||
const DEFAULT_FORMATTER = "{levelName} {msg}";
|
const DEFAULT_FORMATTER = "{levelName} {msg}";
|
||||||
type FormatterFunction = (logRecord: LogRecord) => string;
|
type FormatterFunction = (logRecord: LogRecord) => string;
|
||||||
|
type LogMode = "a" | "w" | "x";
|
||||||
|
|
||||||
interface HandlerOptions {
|
interface HandlerOptions {
|
||||||
formatter?: string | FormatterFunction;
|
formatter?: string | FormatterFunction;
|
||||||
|
@ -85,33 +87,113 @@ export class ConsoleHandler extends BaseHandler {
|
||||||
|
|
||||||
export abstract class WriterHandler extends BaseHandler {
|
export abstract class WriterHandler extends BaseHandler {
|
||||||
protected _writer!: Writer;
|
protected _writer!: Writer;
|
||||||
private _encoder = new TextEncoder();
|
#encoder = new TextEncoder();
|
||||||
|
|
||||||
log(msg: string): void {
|
abstract log(msg: string): void;
|
||||||
this._writer.write(this._encoder.encode(msg + "\n"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileHandlerOptions extends HandlerOptions {
|
interface FileHandlerOptions extends HandlerOptions {
|
||||||
filename: string;
|
filename: string;
|
||||||
|
mode?: LogMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileHandler extends WriterHandler {
|
export class FileHandler extends WriterHandler {
|
||||||
private _file!: File;
|
protected _file!: File;
|
||||||
private _filename: string;
|
protected _filename: string;
|
||||||
|
protected _mode: LogMode;
|
||||||
|
#encoder = new TextEncoder();
|
||||||
|
|
||||||
constructor(levelName: string, options: FileHandlerOptions) {
|
constructor(levelName: string, options: FileHandlerOptions) {
|
||||||
super(levelName, options);
|
super(levelName, options);
|
||||||
this._filename = options.filename;
|
this._filename = options.filename;
|
||||||
|
// default to append mode, write only
|
||||||
|
this._mode = options.mode ? options.mode : "a";
|
||||||
}
|
}
|
||||||
|
|
||||||
async setup(): Promise<void> {
|
async setup(): Promise<void> {
|
||||||
// open file in append mode - write only
|
this._file = await open(this._filename, this._mode);
|
||||||
this._file = await open(this._filename, "a");
|
|
||||||
this._writer = this._file;
|
this._writer = this._file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(msg: string): void {
|
||||||
|
Deno.writeSync(this._file.rid, this.#encoder.encode(msg + "\n"));
|
||||||
|
}
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
async destroy(): Promise<void> {
|
||||||
await this._file.close();
|
await this._file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RotatingFileHandlerOptions extends FileHandlerOptions {
|
||||||
|
maxBytes: number;
|
||||||
|
maxBackupCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RotatingFileHandler extends FileHandler {
|
||||||
|
#maxBytes: number;
|
||||||
|
#maxBackupCount: number;
|
||||||
|
|
||||||
|
constructor(levelName: string, options: RotatingFileHandlerOptions) {
|
||||||
|
super(levelName, options);
|
||||||
|
this.#maxBytes = options.maxBytes;
|
||||||
|
this.#maxBackupCount = options.maxBackupCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup(): Promise<void> {
|
||||||
|
if (this.#maxBytes < 1) {
|
||||||
|
throw new Error("maxBytes cannot be less than 1");
|
||||||
|
}
|
||||||
|
if (this.#maxBackupCount < 1) {
|
||||||
|
throw new Error("maxBackupCount cannot be less than 1");
|
||||||
|
}
|
||||||
|
await super.setup();
|
||||||
|
|
||||||
|
if (this._mode === "w") {
|
||||||
|
// Remove old backups too as it doesn't make sense to start with a clean
|
||||||
|
// log file, but old backups
|
||||||
|
for (let i = 1; i <= this.#maxBackupCount; i++) {
|
||||||
|
if (await exists(this._filename + "." + i)) {
|
||||||
|
await Deno.remove(this._filename + "." + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this._mode === "x") {
|
||||||
|
// Throw if any backups also exist
|
||||||
|
for (let i = 1; i <= this.#maxBackupCount; i++) {
|
||||||
|
if (await exists(this._filename + "." + i)) {
|
||||||
|
Deno.close(this._file.rid);
|
||||||
|
throw new Deno.errors.AlreadyExists(
|
||||||
|
"Backup log file " + this._filename + "." + i + " already exists"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle(logRecord: LogRecord): void {
|
||||||
|
if (this.level > logRecord.level) return;
|
||||||
|
|
||||||
|
const msg = this.format(logRecord);
|
||||||
|
const currentFileSize = statSync(this._filename).size;
|
||||||
|
if (currentFileSize + msg.length > this.#maxBytes) {
|
||||||
|
this.rotateLogFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateLogFiles(): void {
|
||||||
|
close(this._file.rid);
|
||||||
|
|
||||||
|
for (let i = this.#maxBackupCount - 1; i >= 0; i--) {
|
||||||
|
const source = this._filename + (i === 0 ? "" : "." + i);
|
||||||
|
const dest = this._filename + "." + (i + 1);
|
||||||
|
|
||||||
|
if (existsSync(source)) {
|
||||||
|
renameSync(source, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._file = openSync(this._filename, this._mode);
|
||||||
|
this._writer = this._file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
const { test } = Deno;
|
const { test } = Deno;
|
||||||
import { assertEquals } from "../testing/asserts.ts";
|
import { assert, assertEquals, assertThrowsAsync } from "../testing/asserts.ts";
|
||||||
import { LogLevel, getLevelName, getLevelByName } from "./levels.ts";
|
import { LogLevel, getLevelName, getLevelByName } from "./levels.ts";
|
||||||
import { BaseHandler } from "./handlers.ts";
|
import { BaseHandler, FileHandler, RotatingFileHandler } from "./handlers.ts";
|
||||||
|
import { LogRecord } from "./logger.ts";
|
||||||
|
import { existsSync } from "../fs/exists.ts";
|
||||||
|
|
||||||
|
const LOG_FILE = "./test_log.file";
|
||||||
|
|
||||||
class TestHandler extends BaseHandler {
|
class TestHandler extends BaseHandler {
|
||||||
public messages: string[] = [];
|
public messages: string[] = [];
|
||||||
|
@ -47,13 +51,9 @@ test(function simpleHandler(): void {
|
||||||
|
|
||||||
for (const levelName in LogLevel) {
|
for (const levelName in LogLevel) {
|
||||||
const level = getLevelByName(levelName);
|
const level = getLevelByName(levelName);
|
||||||
handler.handle({
|
handler.handle(
|
||||||
msg: `${levelName.toLowerCase()}-test`,
|
new LogRecord(`${levelName.toLowerCase()}-test`, [], level)
|
||||||
args: [],
|
);
|
||||||
datetime: new Date(),
|
|
||||||
level: level,
|
|
||||||
levelName: levelName,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(handler.level, testCase);
|
assertEquals(handler.level, testCase);
|
||||||
|
@ -67,13 +67,7 @@ test(function testFormatterAsString(): void {
|
||||||
formatter: "test {levelName} {msg}",
|
formatter: "test {levelName} {msg}",
|
||||||
});
|
});
|
||||||
|
|
||||||
handler.handle({
|
handler.handle(new LogRecord("Hello, world!", [], LogLevel.DEBUG));
|
||||||
msg: "Hello, world!",
|
|
||||||
args: [],
|
|
||||||
datetime: new Date(),
|
|
||||||
level: LogLevel.DEBUG,
|
|
||||||
levelName: "DEBUG",
|
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(handler.messages, ["test DEBUG Hello, world!"]);
|
assertEquals(handler.messages, ["test DEBUG Hello, world!"]);
|
||||||
});
|
});
|
||||||
|
@ -81,16 +75,229 @@ test(function testFormatterAsString(): void {
|
||||||
test(function testFormatterAsFunction(): void {
|
test(function testFormatterAsFunction(): void {
|
||||||
const handler = new TestHandler("DEBUG", {
|
const handler = new TestHandler("DEBUG", {
|
||||||
formatter: (logRecord): string =>
|
formatter: (logRecord): string =>
|
||||||
`fn formmatter ${logRecord.levelName} ${logRecord.msg}`,
|
`fn formatter ${logRecord.levelName} ${logRecord.msg}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
handler.handle({
|
handler.handle(new LogRecord("Hello, world!", [], LogLevel.ERROR));
|
||||||
msg: "Hello, world!",
|
|
||||||
args: [],
|
|
||||||
datetime: new Date(),
|
|
||||||
level: LogLevel.ERROR,
|
|
||||||
levelName: "ERROR",
|
|
||||||
});
|
|
||||||
|
|
||||||
assertEquals(handler.messages, ["fn formmatter ERROR Hello, world!"]);
|
assertEquals(handler.messages, ["fn formatter ERROR Hello, world!"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "FileHandler with mode 'w' will wipe clean existing log file",
|
||||||
|
async fn() {
|
||||||
|
const fileHandler = new FileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
mode: "w",
|
||||||
|
});
|
||||||
|
|
||||||
|
await fileHandler.setup();
|
||||||
|
fileHandler.handle(new LogRecord("Hello World", [], LogLevel.WARNING));
|
||||||
|
await fileHandler.destroy();
|
||||||
|
const firstFileSize = (await Deno.stat(LOG_FILE)).size;
|
||||||
|
|
||||||
|
await fileHandler.setup();
|
||||||
|
fileHandler.handle(new LogRecord("Hello World", [], LogLevel.WARNING));
|
||||||
|
await fileHandler.destroy();
|
||||||
|
const secondFileSize = (await Deno.stat(LOG_FILE)).size;
|
||||||
|
|
||||||
|
assertEquals(secondFileSize, firstFileSize);
|
||||||
|
Deno.removeSync(LOG_FILE);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "FileHandler with mode 'x' will throw if log file already exists",
|
||||||
|
async fn() {
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
Deno.writeFileSync(LOG_FILE, new TextEncoder().encode("hello world"));
|
||||||
|
const fileHandler = new FileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
mode: "x",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
},
|
||||||
|
Deno.errors.AlreadyExists,
|
||||||
|
"ile exists"
|
||||||
|
);
|
||||||
|
Deno.removeSync(LOG_FILE);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name:
|
||||||
|
"RotatingFileHandler with mode 'w' will wipe clean existing log file and remove others",
|
||||||
|
async fn() {
|
||||||
|
Deno.writeFileSync(LOG_FILE, new TextEncoder().encode("hello world"));
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".1",
|
||||||
|
new TextEncoder().encode("hello world")
|
||||||
|
);
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".2",
|
||||||
|
new TextEncoder().encode("hello world")
|
||||||
|
);
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".3",
|
||||||
|
new TextEncoder().encode("hello world")
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileHandler = new RotatingFileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
maxBytes: 50,
|
||||||
|
maxBackupCount: 3,
|
||||||
|
mode: "w",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
await fileHandler.destroy();
|
||||||
|
|
||||||
|
assertEquals((await Deno.stat(LOG_FILE)).size, 0);
|
||||||
|
assert(!existsSync(LOG_FILE + ".1"));
|
||||||
|
assert(!existsSync(LOG_FILE + ".2"));
|
||||||
|
assert(!existsSync(LOG_FILE + ".3"));
|
||||||
|
|
||||||
|
Deno.removeSync(LOG_FILE);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name:
|
||||||
|
"RotatingFileHandler with mode 'x' will throw if any log file already exists",
|
||||||
|
async fn() {
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".3",
|
||||||
|
new TextEncoder().encode("hello world")
|
||||||
|
);
|
||||||
|
const fileHandler = new RotatingFileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
maxBytes: 50,
|
||||||
|
maxBackupCount: 3,
|
||||||
|
mode: "x",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
},
|
||||||
|
Deno.errors.AlreadyExists,
|
||||||
|
"Backup log file " + LOG_FILE + ".3 already exists"
|
||||||
|
);
|
||||||
|
Deno.removeSync(LOG_FILE + ".3");
|
||||||
|
Deno.removeSync(LOG_FILE);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "RotatingFileHandler with first rollover",
|
||||||
|
async fn() {
|
||||||
|
const fileHandler = new RotatingFileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
maxBytes: 25,
|
||||||
|
maxBackupCount: 3,
|
||||||
|
mode: "w",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
|
||||||
|
fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR)); // 'ERROR AAA\n' = 10 bytes
|
||||||
|
assertEquals((await Deno.stat(LOG_FILE)).size, 10);
|
||||||
|
fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR));
|
||||||
|
assertEquals((await Deno.stat(LOG_FILE)).size, 20);
|
||||||
|
fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR));
|
||||||
|
// Rollover occurred. Log file now has 1 record, rollover file has the original 2
|
||||||
|
assertEquals((await Deno.stat(LOG_FILE)).size, 10);
|
||||||
|
assertEquals((await Deno.stat(LOG_FILE + ".1")).size, 20);
|
||||||
|
|
||||||
|
await fileHandler.destroy();
|
||||||
|
|
||||||
|
Deno.removeSync(LOG_FILE);
|
||||||
|
Deno.removeSync(LOG_FILE + ".1");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "RotatingFileHandler with all backups rollover",
|
||||||
|
async fn() {
|
||||||
|
Deno.writeFileSync(LOG_FILE, new TextEncoder().encode("original log file"));
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".1",
|
||||||
|
new TextEncoder().encode("original log.1 file")
|
||||||
|
);
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".2",
|
||||||
|
new TextEncoder().encode("original log.2 file")
|
||||||
|
);
|
||||||
|
Deno.writeFileSync(
|
||||||
|
LOG_FILE + ".3",
|
||||||
|
new TextEncoder().encode("original log.3 file")
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileHandler = new RotatingFileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
maxBytes: 2,
|
||||||
|
maxBackupCount: 3,
|
||||||
|
mode: "a",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR)); // 'ERROR AAA\n' = 10 bytes
|
||||||
|
await fileHandler.destroy();
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
assertEquals(decoder.decode(Deno.readFileSync(LOG_FILE)), "ERROR AAA\n");
|
||||||
|
assertEquals(
|
||||||
|
decoder.decode(Deno.readFileSync(LOG_FILE + ".1")),
|
||||||
|
"original log file"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
decoder.decode(Deno.readFileSync(LOG_FILE + ".2")),
|
||||||
|
"original log.1 file"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
decoder.decode(Deno.readFileSync(LOG_FILE + ".3")),
|
||||||
|
"original log.2 file"
|
||||||
|
);
|
||||||
|
assert(!existsSync(LOG_FILE + ".4"));
|
||||||
|
|
||||||
|
Deno.removeSync(LOG_FILE);
|
||||||
|
Deno.removeSync(LOG_FILE + ".1");
|
||||||
|
Deno.removeSync(LOG_FILE + ".2");
|
||||||
|
Deno.removeSync(LOG_FILE + ".3");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "RotatingFileHandler maxBytes cannot be less than 1",
|
||||||
|
async fn() {
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
const fileHandler = new RotatingFileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
maxBytes: 0,
|
||||||
|
maxBackupCount: 3,
|
||||||
|
mode: "w",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"maxBytes cannot be less than 1"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "RotatingFileHandler maxBackupCount cannot be less than 1",
|
||||||
|
async fn() {
|
||||||
|
await assertThrowsAsync(
|
||||||
|
async () => {
|
||||||
|
const fileHandler = new RotatingFileHandler("WARNING", {
|
||||||
|
filename: LOG_FILE,
|
||||||
|
maxBytes: 50,
|
||||||
|
maxBackupCount: 0,
|
||||||
|
mode: "w",
|
||||||
|
});
|
||||||
|
await fileHandler.setup();
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"maxBackupCount cannot be less than 1"
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,12 +2,26 @@
|
||||||
import { LogLevel, getLevelByName, getLevelName } from "./levels.ts";
|
import { LogLevel, getLevelByName, getLevelName } from "./levels.ts";
|
||||||
import { BaseHandler } from "./handlers.ts";
|
import { BaseHandler } from "./handlers.ts";
|
||||||
|
|
||||||
export interface LogRecord {
|
export class LogRecord {
|
||||||
msg: string;
|
readonly msg: string;
|
||||||
args: unknown[];
|
#args: unknown[];
|
||||||
datetime: Date;
|
#datetime: Date;
|
||||||
level: number;
|
readonly level: number;
|
||||||
levelName: string;
|
readonly levelName: string;
|
||||||
|
|
||||||
|
constructor(msg: string, args: unknown[], level: number) {
|
||||||
|
this.msg = msg;
|
||||||
|
this.#args = [...args];
|
||||||
|
this.level = level;
|
||||||
|
this.#datetime = new Date();
|
||||||
|
this.levelName = getLevelName(level);
|
||||||
|
}
|
||||||
|
get args(): unknown[] {
|
||||||
|
return [...this.#args];
|
||||||
|
}
|
||||||
|
get datetime(): Date {
|
||||||
|
return new Date(this.#datetime.getTime());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
|
@ -26,16 +40,8 @@ export class Logger {
|
||||||
_log(level: number, msg: string, ...args: unknown[]): void {
|
_log(level: number, msg: string, ...args: unknown[]): void {
|
||||||
if (this.level > level) return;
|
if (this.level > level) return;
|
||||||
|
|
||||||
// TODO: it'd be a good idea to make it immutable, so
|
const record: LogRecord = new LogRecord(msg, args, level);
|
||||||
// no handler mangles it by mistake
|
|
||||||
// TODO: iterpolate msg with values
|
|
||||||
const record: LogRecord = {
|
|
||||||
msg: msg,
|
|
||||||
args: args,
|
|
||||||
datetime: new Date(),
|
|
||||||
level: level,
|
|
||||||
levelName: getLevelName(level),
|
|
||||||
};
|
|
||||||
this.handlers.forEach((handler): void => {
|
this.handlers.forEach((handler): void => {
|
||||||
handler.handle(record);
|
handler.handle(record);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ class TestHandler extends BaseHandler {
|
||||||
public records: LogRecord[] = [];
|
public records: LogRecord[] = [];
|
||||||
|
|
||||||
handle(record: LogRecord): void {
|
handle(record: LogRecord): void {
|
||||||
this.records.push({ ...record });
|
this.records.push(record);
|
||||||
super.handle(record);
|
super.handle(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ConsoleHandler,
|
ConsoleHandler,
|
||||||
WriterHandler,
|
WriterHandler,
|
||||||
FileHandler,
|
FileHandler,
|
||||||
|
RotatingFileHandler,
|
||||||
} from "./handlers.ts";
|
} from "./handlers.ts";
|
||||||
import { assert } from "../testing/asserts.ts";
|
import { assert } from "../testing/asserts.ts";
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ export const handlers = {
|
||||||
ConsoleHandler,
|
ConsoleHandler,
|
||||||
WriterHandler,
|
WriterHandler,
|
||||||
FileHandler,
|
FileHandler,
|
||||||
|
RotatingFileHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLogger(name?: string): Logger {
|
export function getLogger(name?: string): Logger {
|
||||||
|
|
Loading…
Add table
Reference in a new issue