chore: initialize qiming workspace repository
This commit is contained in:
105
qimingclaw/crates/qiming-mcp-stdio-proxy/src/logger.ts
Normal file
105
qimingclaw/crates/qiming-mcp-stdio-proxy/src/logger.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Logging utilities — stderr only, stdout is the MCP JSON-RPC channel.
|
||||
*
|
||||
* When the environment variable MCP_PROXY_LOG_FILE is set, log lines are
|
||||
* also appended to that file so the Electron host can tail them into main.log.
|
||||
*
|
||||
* Log rotation: file is named by date (e.g. mcp-proxy-2026-03-09.log).
|
||||
* A new file is created each day. Old files beyond MAX_LOG_FILES are deleted.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const logFilePath = process.env.MCP_PROXY_LOG_FILE;
|
||||
const MAX_LOG_FILES = 7;
|
||||
|
||||
let logStream: fs.WriteStream | null = null;
|
||||
let logDir = '';
|
||||
let logBaseName = '';
|
||||
let logExt = '';
|
||||
let currentDateStr = '';
|
||||
|
||||
function dateStr(): string {
|
||||
const d = new Date();
|
||||
const pad2 = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
function openLogFile(): void {
|
||||
if (!logFilePath) return;
|
||||
|
||||
const today = dateStr();
|
||||
if (today === currentDateStr && logStream) return;
|
||||
|
||||
// Close previous stream
|
||||
if (logStream) {
|
||||
try { logStream.end(); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
currentDateStr = today;
|
||||
const dated = path.join(logDir, `${logBaseName}-${today}${logExt}`);
|
||||
try {
|
||||
logStream = fs.createWriteStream(dated, { flags: 'a' });
|
||||
} catch {
|
||||
logStream = null;
|
||||
}
|
||||
|
||||
// Cleanup old log files
|
||||
cleanupOldLogs();
|
||||
}
|
||||
|
||||
function cleanupOldLogs(): void {
|
||||
if (!logDir || !logBaseName) return;
|
||||
try {
|
||||
const prefix = `${logBaseName}-`;
|
||||
const files = fs.readdirSync(logDir)
|
||||
.filter(f => f.startsWith(prefix) && f.endsWith(logExt))
|
||||
.sort()
|
||||
.reverse();
|
||||
for (let i = MAX_LOG_FILES; i < files.length; i++) {
|
||||
try { fs.unlinkSync(path.join(logDir, files[i])); } catch { /* ignore */ }
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (logFilePath) {
|
||||
try {
|
||||
logDir = path.dirname(logFilePath);
|
||||
if (logDir && !fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
const fullName = path.basename(logFilePath);
|
||||
const dotIdx = fullName.lastIndexOf('.');
|
||||
if (dotIdx > 0) {
|
||||
logBaseName = fullName.substring(0, dotIdx);
|
||||
logExt = fullName.substring(dotIdx);
|
||||
} else {
|
||||
logBaseName = fullName;
|
||||
logExt = '.log';
|
||||
}
|
||||
openLogFile();
|
||||
} catch {
|
||||
// If file creation fails, continue without file logging
|
||||
}
|
||||
}
|
||||
|
||||
function timestamp(): string {
|
||||
const d = new Date();
|
||||
const pad2 = (n: number) => String(n).padStart(2, '0');
|
||||
const pad3 = (n: number) => String(n).padStart(3, '0');
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
|
||||
}
|
||||
|
||||
export function log(level: string, msg: string): void {
|
||||
const line = `[${timestamp()}] [${level.toLowerCase()}] [qiming-mcp-proxy] ${msg}\n`;
|
||||
process.stderr.write(line);
|
||||
if (logFilePath) {
|
||||
openLogFile(); // Rotate if date changed
|
||||
logStream?.write(line);
|
||||
}
|
||||
}
|
||||
|
||||
export const logInfo = (msg: string) => log('INFO', msg);
|
||||
export const logWarn = (msg: string) => log('WARN', msg);
|
||||
export const logError = (msg: string) => log('ERROR', msg);
|
||||
Reference in New Issue
Block a user