route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
414 lines • 17.6 kB
JavaScript
"use strict";
/**
* 统一日志系统 - 项目唯一日志入口
* 整合所有现有日志功能,提供统一接口
* Project Owner: Jason Zhang
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnifiedLogger = void 0;
const fs_1 = require("fs");
const path = __importStar(require("path"));
const os = __importStar(require("os"));
class UnifiedLogger {
config;
logDir;
currentRotationDir = '';
lastRotationTime = 0;
initialized = false;
constructor(config) {
this.config = {
port: config.port,
logLevel: config.logLevel || 'info',
enableConsole: config.enableConsole ?? true,
enableFile: config.enableFile ?? true,
baseDir: config.baseDir || path.join(os.homedir(), '.route-claude-code', 'logs'),
rotationMinutes: config.rotationMinutes || 5,
maxRetentionDays: config.maxRetentionDays || 7
};
this.logDir = path.join(this.config.baseDir, `port-${this.config.port}`);
// 异步初始化,但不等待完成
this.initialize().catch(error => {
console.error('Failed to initialize unified logger:', error);
});
}
async initialize() {
if (this.initialized)
return;
try {
await fs_1.promises.mkdir(this.logDir, { recursive: true });
this.updateRotationDir();
await fs_1.promises.mkdir(this.currentRotationDir, { recursive: true });
this.initialized = true;
}
catch (error) {
console.error('Failed to initialize unified logger:', error);
}
}
updateRotationDir() {
const now = new Date();
const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const timeStr = beijingTime.toISOString().replace(/[:.]/g, '-').slice(0, 19);
this.currentRotationDir = path.join(this.logDir, timeStr);
this.lastRotationTime = Date.now();
}
async ensureRotation() {
const rotationInterval = this.config.rotationMinutes * 60 * 1000;
if (Date.now() - this.lastRotationTime > rotationInterval) {
this.updateRotationDir();
await fs_1.promises.mkdir(this.currentRotationDir, { recursive: true });
}
}
getBeijingTime() {
const now = new Date();
const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
return beijingTime.toISOString().replace('T', ' ').slice(0, 19);
}
shouldLog(level) {
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
return levels[level] <= levels[this.config.logLevel];
}
formatEntry(category, level, message, data, requestId, stage, duration) {
return {
timestamp: new Date().toISOString(),
beijingTime: this.getBeijingTime(),
level,
category,
message,
data,
port: this.config.port,
requestId,
stage,
duration
};
}
async writeToFile(entry) {
if (!this.config.enableFile)
return;
const maxRetries = 3;
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// 确保初始化完成
if (!this.initialized) {
await this.initialize();
}
await this.ensureRotation();
// 强制确保目录结构存在
await fs_1.promises.mkdir(this.logDir, { recursive: true });
await fs_1.promises.mkdir(this.currentRotationDir, { recursive: true });
const filename = `${entry.category}.log`;
const filepath = path.join(this.currentRotationDir, filename);
const logLine = JSON.stringify(entry) + '\n';
await fs_1.promises.appendFile(filepath, logLine, 'utf-8');
return; // 成功写入,退出重试循环
}
catch (error) {
lastError = error;
if (attempt < maxRetries - 1) {
// 等待一小段时间后重试
await new Promise(resolve => setTimeout(resolve, 10 * (attempt + 1)));
// 重置初始化状态,强制重新初始化
this.initialized = false;
}
}
}
// 所有重试都失败了
console.error(`Failed to write log after ${maxRetries} attempts:`, lastError);
console.error('Log entry that failed to write:', {
category: entry.category,
level: entry.level,
message: entry.message,
port: entry.port,
logDir: this.logDir,
currentRotationDir: this.currentRotationDir
});
}
// 专门写入finish reason到独立文件
async writeToFinishReasonFile(entry) {
if (!this.config.enableFile)
return;
const maxRetries = 3;
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// 确保初始化完成
if (!this.initialized) {
await this.initialize();
}
await this.ensureRotation();
// 强制确保目录结构存在
await fs_1.promises.mkdir(this.logDir, { recursive: true });
await fs_1.promises.mkdir(this.currentRotationDir, { recursive: true });
// 写入到独立的finish_reason.log文件
const filename = 'finish_reason.log';
const filepath = path.join(this.currentRotationDir, filename);
const logLine = JSON.stringify(entry) + '\n';
await fs_1.promises.appendFile(filepath, logLine, 'utf-8');
return; // 成功写入,退出重试循环
}
catch (error) {
lastError = error;
if (attempt < maxRetries - 1) {
// 等待一小段时间后重试
await new Promise(resolve => setTimeout(resolve, 10 * (attempt + 1)));
// 重置初始化状态,强制重新初始化
this.initialized = false;
}
}
}
// 所有重试都失败了
console.error(`Failed to write finish reason log after ${maxRetries} attempts:`, lastError);
}
// 直接写入到finish-reason-debug.log文件(用于双重记录)
async writeToFinishReasonDebugFile(entry) {
if (!this.config.enableFile)
return;
try {
// 确保初始化完成
if (!this.initialized) {
await this.initialize();
}
// 写入到独立的finish-reason-debug.log文件(不使用轮转目录)
const filename = 'finish-reason-debug.log';
const filepath = path.join(this.logDir, filename);
const logLine = JSON.stringify(entry) + '\n';
await fs_1.promises.appendFile(filepath, logLine, 'utf-8');
}
catch (error) {
console.error('Failed to write finish reason debug log:', error);
}
}
outputToConsole(entry) {
if (!this.config.enableConsole)
return;
const { beijingTime, level, category, message, stage, duration, requestId } = entry;
const timeShort = beijingTime.split(' ')[1];
let output = `[${timeShort}] [${level.toUpperCase()}] [${category}]`;
if (requestId)
output += ` [${requestId}]`;
if (stage)
output += ` [${stage}]`;
output += ` ${message}`;
if (duration)
output += ` (${duration}ms)`;
switch (level) {
case 'error':
console.error(output);
break;
case 'warn':
console.warn(output);
break;
default:
console.log(output);
break;
}
}
// 核心日志方法
log(category, level, message, data, requestId, stage, duration) {
if (!this.shouldLog(level))
return;
const entry = this.formatEntry(category, level, message, data, requestId, stage, duration);
this.outputToConsole(entry);
// Fix: Properly handle async writeToFile operation
this.writeToFile(entry).catch(error => {
console.error('Failed to write log entry:', error);
});
}
// 便捷方法
error(message, data, requestId, stage) {
this.log('error', 'error', message, data, requestId, stage);
}
warn(message, data, requestId, stage) {
this.log('system', 'warn', message, data, requestId, stage);
}
info(message, data, requestId, stage) {
this.log('system', 'info', message, data, requestId, stage);
}
debug(message, data, requestId, stage) {
this.log('system', 'debug', message, data, requestId, stage);
}
// 专用日志方法
logRequest(requestId, method, path, data) {
this.log('request', 'info', `${method} ${path}`, data, requestId, 'request');
}
logResponse(requestId, status, data, duration) {
this.log('response', 'info', `Status ${status}`, data, requestId, 'response', duration);
}
logPipeline(stage, message, data, requestId) {
this.log('pipeline', 'info', message, data, requestId, stage);
}
logPerformance(operation, duration, data, requestId) {
this.log('performance', 'info', `${operation} completed`, data, requestId, operation, duration);
}
logToolCall(message, data, requestId, stage) {
this.log('tool_call', 'info', message, data, requestId, stage);
}
logStreaming(message, data, requestId, stage) {
this.log('streaming', 'debug', message, data, requestId, stage);
}
// 专门记录finish reason - 强制记录,不受日志级别限制
logFinishReason(finishReason, data, requestId, stage) {
// 强制记录finish reason,不论日志级别如何
const entry = this.formatEntry('finish_reason', 'info', `⚪ [SINGLE-FINISH-REASON] ${finishReason}`, {
type: 'single_finish_reason',
finishReason,
timestamp: new Date().toISOString(),
...data
}, requestId, stage);
// 始终输出到控制台(如果启用)
this.outputToConsole(entry);
// 强制写入到独立的finish reason文件
this.writeToFinishReasonFile(entry).catch(error => {
console.error('Failed to write finish reason log entry:', error);
});
// 同时写入到常规日志文件
this.writeToFile(entry).catch(error => {
console.error('Failed to write finish reason to main log:', error);
});
}
// 增强版:同时记录原始服务器返回的reason和转换后的reason
logDualFinishReason(originalReason, convertedReason, provider, data, requestId, stage) {
// 🎯 首先记录原始服务器响应
const originalEntry = this.formatEntry('finish_reason', 'info', `🔵 [ORIGINAL-SERVER-RESPONSE] ${provider} returned: "${originalReason}"`, {
type: 'original_server_response',
originalFinishReason: originalReason,
provider,
serverResponseType: 'raw_finish_reason',
timestamp: new Date().toISOString(),
...data
}, requestId, `${stage}_original`);
// 🎯 然后记录转换后的结果
const convertedEntry = this.formatEntry('finish_reason', 'info', `🟢 [CONVERTED-ANTHROPIC-FORMAT] Transformed to: "${convertedReason}"`, {
type: 'converted_anthropic_format',
convertedStopReason: convertedReason,
provider,
conversionTarget: 'anthropic_stop_reason',
timestamp: new Date().toISOString(),
...data
}, requestId, `${stage}_converted`);
// 🎯 最后记录完整的转换映射
const mappingEntry = this.formatEntry('finish_reason', 'info', `🔄 [CONVERSION-MAPPING] ${originalReason} ═══════► ${convertedReason}`, {
type: 'conversion_mapping',
originalFinishReason: originalReason,
convertedStopReason: convertedReason,
provider,
conversionMapping: `${originalReason} ═══════► ${convertedReason}`,
conversionDirection: 'server_to_anthropic',
timestamp: new Date().toISOString(),
...data
}, requestId, `${stage}_mapping`);
// 输出到控制台(三条分隔的记录)
console.log('\n' + '='.repeat(80));
console.log('🔍 DUAL FINISH REASON LOGGING');
console.log('='.repeat(80));
this.outputToConsole(originalEntry);
this.outputToConsole(convertedEntry);
this.outputToConsole(mappingEntry);
console.log('='.repeat(80) + '\n');
// 强制写入到独立的finish reason文件(按顺序写入三条记录)
Promise.all([
this.writeToFinishReasonFile(originalEntry),
this.writeToFinishReasonFile(convertedEntry),
this.writeToFinishReasonFile(mappingEntry)
]).catch(error => {
console.error('Failed to write dual finish reason log entries:', error);
});
// 同时写入到常规日志文件
Promise.all([
this.writeToFile(originalEntry),
this.writeToFile(convertedEntry),
this.writeToFile(mappingEntry)
]).catch(error => {
console.error('Failed to write dual finish reason to main log:', error);
});
// 同时写入到finish-reason-debug.log文件
Promise.all([
this.writeToFinishReasonDebugFile(originalEntry),
this.writeToFinishReasonDebugFile(convertedEntry),
this.writeToFinishReasonDebugFile(mappingEntry)
]).catch(error => {
console.error('Failed to write dual finish reason to debug log:', error);
});
}
// 专门记录stop reason - 强制记录,不受日志级别限制
logStopReason(stopReason, data, requestId, stage) {
// 强制记录stop reason,不论日志级别如何
const entry = this.formatEntry('finish_reason', 'info', `⭕ [SINGLE-STOP-REASON] ${stopReason}`, {
type: 'single_stop_reason',
stopReason,
timestamp: new Date().toISOString(),
...data
}, requestId, stage);
// 始终输出到控制台(如果启用)
this.outputToConsole(entry);
// 强制写入到独立的finish reason文件
this.writeToFinishReasonFile(entry).catch(error => {
console.error('Failed to write stop reason log entry:', error);
});
// 同时写入到常规日志文件
this.writeToFile(entry).catch(error => {
console.error('Failed to write stop reason to main log:', error);
});
}
// 清理方法
async cleanup() {
try {
const dirs = await fs_1.promises.readdir(this.logDir);
const cutoffTime = Date.now() - (this.config.maxRetentionDays * 24 * 60 * 60 * 1000);
let cleaned = 0;
for (const dir of dirs) {
if (dir.match(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)) {
const dirPath = path.join(this.logDir, dir);
const stats = await fs_1.promises.stat(dirPath);
if (stats.birthtimeMs < cutoffTime) {
await fs_1.promises.rm(dirPath, { recursive: true });
cleaned++;
}
}
}
return cleaned;
}
catch (error) {
console.error('Failed to cleanup logs:', error);
return 0;
}
}
async shutdown() {
this.info('Logger shutting down');
}
}
exports.UnifiedLogger = UnifiedLogger;
//# sourceMappingURL=unified-logger.js.map