Files
AstrBot/desktop/lib/buffered-rotating-logger.js
エイカク dc995af34b fix(desktop): 为 Electron 与后端日志增加按大小轮转 (#5029)
* fix(desktop): rotate electron and backend logs

* refactor(desktop): centralize log rotation defaults and debug fs errors

* fix(desktop): harden rotation fs ops and buffer backend log writes

* refactor(desktop): extract buffered logger and reduce sync stat calls

* refactor(desktop): simplify rotation flow and harden logger config

* fix(desktop): make app logging async and flush-safe

* fix: harden app log path switching and debug-gated rotation errors

* fix: cap buffered log chunk size during path switch
2026-02-11 20:17:57 +09:00

163 lines
3.9 KiB
JavaScript

'use strict';
const { RotatingLogWriter } = require('./rotating-log-writer');
const { parseEnvInt } = require('./common');
const DEFAULT_FLUSH_INTERVAL_MS = 120;
const DEFAULT_MAX_BUFFER_BYTES = 128 * 1024;
const MIN_FLUSH_INTERVAL_MS = 10;
const MIN_MAX_BUFFER_BYTES = 4 * 1024;
const MAX_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
function clampIntOption(raw, { defaultValue, min, max }) {
const value = parseEnvInt(raw, defaultValue);
return Math.min(Math.max(value, min), max);
}
class BufferedRotatingLogger {
constructor({
logPath = null,
maxBytes,
backupCount,
flushIntervalMs,
maxBufferBytes,
label = 'buffered-log',
}) {
this.logPath = logPath || null;
this.flushIntervalMs = clampIntOption(flushIntervalMs, {
defaultValue: DEFAULT_FLUSH_INTERVAL_MS,
min: MIN_FLUSH_INTERVAL_MS,
max: 60 * 1000,
});
this.maxBufferBytes = clampIntOption(maxBufferBytes, {
defaultValue: DEFAULT_MAX_BUFFER_BYTES,
min: MIN_MAX_BUFFER_BYTES,
max: MAX_MAX_BUFFER_BYTES,
});
this.buffer = [];
this.bufferBytes = 0;
this.flushTimer = null;
this.pathSwitch = Promise.resolve();
this.writer = new RotatingLogWriter({
logPath: this.logPath,
maxBytes,
backupCount,
label,
});
}
setLogPath(logPath) {
const nextLogPath = logPath || null;
this.pathSwitch = this.pathSwitch.then(async () => {
if (nextLogPath === this.logPath) {
await this.flush();
return;
}
const previousLogPath = this.logPath;
if (previousLogPath) {
await this.flush();
}
this.logPath = null;
await this.writer.setLogPath(nextLogPath);
this.logPath = nextLogPath;
await this.flush();
});
return this.pathSwitch;
}
log(payload) {
if (payload === undefined || payload === null) {
return;
}
const chunk = Buffer.isBuffer(payload)
? payload
: Buffer.from(String(payload), 'utf8');
if (!chunk.length) {
return;
}
if (!this.logPath) {
const boundedChunk = this.clipChunkToBufferLimit(chunk);
this.dropOldestUntilWithinLimit(boundedChunk.length);
this.buffer.push(boundedChunk);
this.bufferBytes += boundedChunk.length;
return;
}
this.buffer.push(chunk);
this.bufferBytes += chunk.length;
if (this.bufferBytes >= this.maxBufferBytes) {
void this.flush();
return;
}
this.scheduleFlush();
}
flush() {
this.clearFlushTimer();
if (!this.buffer.length) {
return this.writer.flush();
}
if (!this.logPath) {
// Path is switching or temporarily unavailable; keep buffered data.
this.dropOldestUntilWithinLimit(0);
return this.writer.flush();
}
const chunks = this.buffer;
this.buffer = [];
this.bufferBytes = 0;
const payload = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks);
this.writer.append(payload);
return this.writer.flush();
}
dropOldestUntilWithinLimit(incomingBytes = 0) {
while (
this.buffer.length &&
this.bufferBytes + Math.max(0, incomingBytes) > this.maxBufferBytes
) {
const removed = this.buffer.shift();
if (removed) {
this.bufferBytes -= removed.length;
}
}
if (this.bufferBytes < 0) {
this.bufferBytes = 0;
}
}
clipChunkToBufferLimit(chunk) {
if (chunk.length <= this.maxBufferBytes) {
return chunk;
}
return chunk.subarray(chunk.length - this.maxBufferBytes);
}
scheduleFlush() {
if (this.flushTimer !== null) {
return;
}
this.flushTimer = setTimeout(() => {
this.flushTimer = null;
void this.flush();
}, this.flushIntervalMs);
this.flushTimer.unref?.();
}
clearFlushTimer() {
if (this.flushTimer === null) {
return;
}
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
}
module.exports = {
BufferedRotatingLogger,
};