std/log/rotating_file_handler.ts

140 lines
4.9 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import type { LevelName } from "./levels.ts";
import { existsSync } from "@std/fs/exists";
import { FileHandler, type FileHandlerOptions } from "./file_handler.ts";
import {
encoderSymbol,
filenameSymbol,
fileSymbol,
modeSymbol,
openOptionsSymbol,
} from "./_file_handler_symbols.ts";
interface RotatingFileHandlerOptions extends FileHandlerOptions {
maxBytes: number;
maxBackupCount: number;
}
/**
* This handler extends the functionality of the {@linkcode 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.
*
* This handler uses a buffer for writing log messages to file. Logs can be
* manually flushed with `fileHandler.flush()`. Log messages with a log level
* greater than ERROR are immediately flushed. Logs are also flushed on process
* completion.
*
* 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.
*/
export class RotatingFileHandler extends FileHandler {
#maxBytes: number;
#maxBackupCount: number;
#currentFileSize = 0;
constructor(levelName: LevelName, options: RotatingFileHandlerOptions) {
super(levelName, options);
this.#maxBytes = options.maxBytes;
this.#maxBackupCount = options.maxBackupCount;
}
override setup() {
if (this.#maxBytes < 1) {
this.destroy();
throw new Error(`"maxBytes" must be >= 1: received ${this.#maxBytes}`);
}
if (this.#maxBackupCount < 1) {
this.destroy();
throw new Error(
`"maxBackupCount" must be >= 1: received ${this.#maxBackupCount}`,
);
}
super.setup();
if (this[modeSymbol] === "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++) {
try {
Deno.removeSync(this[filenameSymbol] + "." + i);
} catch (error) {
if (!(error instanceof Deno.errors.NotFound)) {
throw error;
}
}
}
} else if (this[modeSymbol] === "x") {
// Throw if any backups also exist
for (let i = 1; i <= this.#maxBackupCount; i++) {
if (existsSync(this[filenameSymbol] + "." + i)) {
this.destroy();
throw new Deno.errors.AlreadyExists(
"Backup log file " + this[filenameSymbol] + "." + i +
" already exists",
);
}
}
} else {
this.#currentFileSize = (Deno.statSync(this[filenameSymbol])).size;
}
}
override log(msg: string) {
const msgByteLength = this[encoderSymbol].encode(msg).byteLength + 1;
if (this.#currentFileSize + msgByteLength > this.#maxBytes) {
this.rotateLogFiles();
this.#currentFileSize = 0;
}
super.log(msg);
this.#currentFileSize += msgByteLength;
}
rotateLogFiles() {
this.flush();
this[fileSymbol]!.close();
for (let i = this.#maxBackupCount - 1; i >= 0; i--) {
const source = this[filenameSymbol] + (i === 0 ? "" : "." + i);
const dest = this[filenameSymbol] + "." + (i + 1);
if (existsSync(source)) {
Deno.renameSync(source, dest);
}
}
this[fileSymbol] = Deno.openSync(
this[filenameSymbol],
this[openOptionsSymbol],
);
}
}