feat: add structured logging, process watchdog, and systemd hardening
- Add lib/logger.js: timestamped structured logging with 5MB x 5 file rotation - Add lib/watchdog.js: generic child process supervisor with rate-limited restarts - Enhance client.js: WS ping/pong liveness detection, uncaughtException/unhandledRejection handlers, systemd sd-notify integration - Refactor frpc.js: FrpcManager now delegates to Watchdog instead of manual spawn/exit - Enhance install.sh: environment file, log directory, systemd resource limits, security hardening, WatchdogSec=60 - Replace all console.log/warn/error with structured logger across modules Made-with: Cursor
This commit is contained in:
118
lib/logger.js
Normal file
118
lib/logger.js
Normal file
@@ -0,0 +1,118 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
|
||||
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|
||||
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
|
||||
|
||||
const LOG_DIR = process.env.CLAWD_LOG_DIR || path.join(CONFIG_DIR, 'logs');
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
const MAX_FILES = 5;
|
||||
|
||||
class Logger {
|
||||
constructor(opts = {}) {
|
||||
this._level = LEVELS[opts.level || process.env.CLAWD_LOG_LEVEL || 'info'] ?? LEVELS.info;
|
||||
this._logToFile = opts.logToFile ?? (process.env.CLAWD_LOG_FILE !== '0');
|
||||
this._stream = null;
|
||||
this._filePath = null;
|
||||
this._fileSize = 0;
|
||||
|
||||
if (this._logToFile) {
|
||||
this._ensureLogDir();
|
||||
}
|
||||
}
|
||||
|
||||
debug(tag, ...args) { this._log('debug', tag, args); }
|
||||
info(tag, ...args) { this._log('info', tag, args); }
|
||||
warn(tag, ...args) { this._log('warn', tag, args); }
|
||||
error(tag, ...args) { this._log('error', tag, args); }
|
||||
|
||||
_log(level, tag, args) {
|
||||
if (LEVELS[level] < this._level) return;
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
const lvl = level.toUpperCase().padEnd(5);
|
||||
const body = args.map(a => (a instanceof Error ? a.stack || a.message : String(a))).join(' ');
|
||||
const line = `${ts} ${lvl} [${tag}] ${body}`;
|
||||
|
||||
const consoleFn = level === 'error' ? console.error
|
||||
: level === 'warn' ? console.warn
|
||||
: console.log;
|
||||
consoleFn(line);
|
||||
|
||||
if (this._logToFile) this._writeToFile(line + '\n');
|
||||
}
|
||||
|
||||
_ensureLogDir() {
|
||||
try { fs.mkdirSync(LOG_DIR, { recursive: true }); }
|
||||
catch (_) { this._logToFile = false; }
|
||||
}
|
||||
|
||||
_writeToFile(line) {
|
||||
if (!this._stream) this._openFile();
|
||||
if (!this._stream) return;
|
||||
|
||||
this._stream.write(line);
|
||||
this._fileSize += Buffer.byteLength(line);
|
||||
|
||||
if (this._fileSize >= MAX_FILE_SIZE) this._rotate();
|
||||
}
|
||||
|
||||
_openFile() {
|
||||
try {
|
||||
this._filePath = path.join(LOG_DIR, 'clawd.log');
|
||||
try {
|
||||
const stat = fs.statSync(this._filePath);
|
||||
this._fileSize = stat.size;
|
||||
} catch (_) { this._fileSize = 0; }
|
||||
|
||||
this._stream = fs.createWriteStream(this._filePath, { flags: 'a' });
|
||||
this._stream.on('error', () => {
|
||||
this._logToFile = false;
|
||||
this._stream = null;
|
||||
});
|
||||
} catch (_) {
|
||||
this._logToFile = false;
|
||||
}
|
||||
}
|
||||
|
||||
_rotate() {
|
||||
if (this._stream) {
|
||||
this._stream.end();
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
// clawd.log.4 → delete, clawd.log.3 → .4, ... clawd.log → .1
|
||||
for (let i = MAX_FILES - 1; i >= 1; i--) {
|
||||
const from = path.join(LOG_DIR, `clawd.log.${i}`);
|
||||
const to = path.join(LOG_DIR, `clawd.log.${i + 1}`);
|
||||
try { fs.renameSync(from, to); } catch (_) {}
|
||||
}
|
||||
try {
|
||||
fs.renameSync(this._filePath, path.join(LOG_DIR, 'clawd.log.1'));
|
||||
} catch (_) {}
|
||||
|
||||
// 删除超出上限的文件
|
||||
try {
|
||||
fs.unlinkSync(path.join(LOG_DIR, `clawd.log.${MAX_FILES + 1}`));
|
||||
} catch (_) {}
|
||||
|
||||
this._fileSize = 0;
|
||||
this._openFile();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this._stream) {
|
||||
this._stream.end();
|
||||
this._stream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
module.exports = logger;
|
||||
Reference in New Issue
Block a user