1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139 |
 
 
x17
x17
x17
x17
x17
x17
x17
x17
x17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x17
x17
x27
x17
 
x17
x27
x27
x27
x27
 
x17
x27
x28
x28
x28
x27
x28
x28
x28
 
x28
x35
 
x27
 
 
x33
x63
x63
x63
 
 
 
x90
x63
x27
 
x30
x33
x34
x34
x34
x34
 
x34
x33
 
x30
x30
x27
 
x17
x30
 
x30
x34
x34
x34
 
x30
 
x30
x30
 
x17
x21
x21
 
x21
x31
x31
 
x31
x37
x37
x31
 
x21
x21
x21
 
x21
x17 |
I
I
|
// Copyright 2018-2025 the Deno authors. 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],
);
}
}
|