Skip to content

Commit b638b34

Browse files
committed
feat(mongodb-log-writer): add logRetentionGB configuration MONGOSH-1985
1 parent 95d3673 commit b638b34

File tree

2 files changed

+90
-21
lines changed

2 files changed

+90
-21
lines changed

packages/mongodb-log-writer/src/mongo-log-manager.spec.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ describe('MongoLogManager', function () {
8686
}
8787
});
8888

89+
const getFilesState = async (paths: string[]) => {
90+
return (
91+
await Promise.all(
92+
paths.map((path) =>
93+
fs.stat(path).then(
94+
() => 1,
95+
() => 0
96+
)
97+
)
98+
)
99+
).join('');
100+
};
101+
89102
it('cleans up least recent log files when requested', async function () {
90103
const manager = new MongoLogManager({
91104
directory,
@@ -106,21 +119,38 @@ describe('MongoLogManager', function () {
106119
paths.unshift(filename);
107120
}
108121

109-
const getFiles = async () => {
110-
return (
111-
await Promise.all(
112-
paths.map((path) =>
113-
fs.stat(path).then(
114-
() => 1,
115-
() => 0
116-
)
117-
)
118-
)
119-
).join('');
120-
};
121-
expect(await getFiles()).to.equal('1111111111');
122+
expect(await getFilesState(paths)).to.equal('1111111111');
123+
await manager.cleanupOldLogFiles();
124+
expect(await getFilesState(paths)).to.equal('0000011111');
125+
});
126+
127+
it('cleans up least recent log files when requested with a storage limit', async function () {
128+
const manager = new MongoLogManager({
129+
directory,
130+
retentionDays,
131+
maxLogFileCount: 1000,
132+
// 6 KB
133+
logRetentionGB: 6 / 1024 / 1024,
134+
onwarn,
135+
onerror,
136+
});
137+
138+
const paths: string[] = [];
139+
const offset = Math.floor(Date.now() / 1000);
140+
141+
// Create 10 files of 1 KB each.
142+
for (let i = 0; i < 10; i++) {
143+
const filename = path.join(
144+
directory,
145+
ObjectId.createFromTime(offset - i).toHexString() + '_log'
146+
);
147+
await fs.writeFile(filename, '0'.repeat(1024));
148+
paths.unshift(filename);
149+
}
150+
151+
expect(await getFilesState(paths)).to.equal('1111111111');
122152
await manager.cleanupOldLogFiles();
123-
expect(await getFiles()).to.equal('0000011111');
153+
expect(await getFilesState(paths)).to.equal('0000111111');
124154
});
125155

126156
it('cleaning up old log files is a no-op by default', async function () {

packages/mongodb-log-writer/src/mongo-log-manager.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface MongoLogOptions {
1717
retentionDays: number;
1818
/** The maximal number of log files which are kept. */
1919
maxLogFileCount?: number;
20+
/** The maximal GB of log files which are kept. */
21+
logRetentionGB?: number;
2022
/** A handler for warnings related to a specific filesystem path. */
2123
onerror: (err: Error, path: string) => unknown | Promise<void>;
2224
/** A handler for errors related to a specific filesystem path. */
@@ -54,8 +56,15 @@ export class MongoLogManager {
5456
const leastRecentFileHeap = new Heap<{
5557
fileTimestamp: number;
5658
fullPath: string;
59+
fileSize?: number;
5760
}>((a, b) => a.fileTimestamp - b.fileTimestamp);
5861

62+
const storageSizeLimit = this._options.logRetentionGB
63+
? this._options.logRetentionGB * 1024 * 1024 * 1024
64+
: Infinity;
65+
let usedStorageSize = this._options.logRetentionGB ? 0 : -Infinity;
66+
// eslint-disable-next-line no-console
67+
5968
for await (const dirent of dirHandle) {
6069
// Cap the overall time spent inside this function. Consider situations like
6170
// a large number of machines using a shared network-mounted $HOME directory
@@ -69,23 +78,53 @@ export class MongoLogManager {
6978
if (!id) continue;
7079
const fileTimestamp = +new ObjectId(id).getTimestamp();
7180
const fullPath = path.join(dir, dirent.name);
72-
let toDelete: string | undefined;
81+
let toDelete:
82+
| {
83+
fullPath: string;
84+
/** If the file wasn't deleted right away and there is a
85+
* retention size limit, its size should be accounted */
86+
fileSize?: number;
87+
}
88+
| undefined;
7389

7490
// If the file is older than expected, delete it. If the file is recent,
7591
// add it to the list of seen files, and if that list is too large, remove
7692
// the least recent file we've seen so far.
7793
if (fileTimestamp < deletionCutoffTimestamp) {
78-
toDelete = fullPath;
79-
} else if (this._options.maxLogFileCount) {
80-
leastRecentFileHeap.push({ fullPath, fileTimestamp });
81-
if (leastRecentFileHeap.size() > this._options.maxLogFileCount) {
82-
toDelete = leastRecentFileHeap.pop()?.fullPath;
94+
toDelete = {
95+
fullPath,
96+
};
97+
} else if (
98+
this._options.logRetentionGB ||
99+
this._options.maxLogFileCount
100+
) {
101+
const fileSize = (await fs.stat(fullPath)).size;
102+
if (this._options.logRetentionGB) {
103+
usedStorageSize += fileSize;
104+
}
105+
106+
leastRecentFileHeap.push({
107+
fullPath,
108+
fileTimestamp,
109+
fileSize,
110+
});
111+
112+
const reachedMaxStorageSize = usedStorageSize > storageSizeLimit;
113+
const reachedMaxFileCount =
114+
this._options.maxLogFileCount &&
115+
leastRecentFileHeap.size() > this._options.maxLogFileCount;
116+
117+
if (reachedMaxStorageSize || reachedMaxFileCount) {
118+
toDelete = leastRecentFileHeap.pop();
83119
}
84120
}
85121

86122
if (!toDelete) continue;
87123
try {
88-
await fs.unlink(toDelete);
124+
await fs.unlink(toDelete.fullPath);
125+
if (toDelete.fileSize) {
126+
usedStorageSize -= toDelete.fileSize;
127+
}
89128
// eslint-disable-next-line @typescript-eslint/no-explicit-any
90129
} catch (err: any) {
91130
if (err?.code !== 'ENOENT') {

0 commit comments

Comments
 (0)