mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
140 lines
4.9 KiB
TypeScript
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],
|
|
);
|
|
}
|
|
}
|