@tiflux/mcp
Version:
TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients
327 lines (288 loc) • 8.56 kB
JavaScript
/**
* Structured Logger
* Sistema de logging estruturado com diferentes níveis e formatos
*/
const fs = require('fs');
const path = require('path');
class Logger {
constructor(config = {}) {
this.config = {
level: config.level || 'info',
format: config.format || 'json',
enableConsole: config.enableConsole !== false,
enableFile: config.enableFile || false,
fileName: config.fileName || 'logs/tiflux-mcp.log',
maxFileSize: config.maxFileSize || 10 * 1024 * 1024, // 10MB
maxFiles: config.maxFiles || 5,
...config
};
this.levels = {
error: 0,
warn: 1,
info: 2,
debug: 3
};
this.currentLevel = this.levels[this.config.level] || this.levels.info;
// Criar diretório de logs se necessário
if (this.config.enableFile) {
this.ensureLogDirectory();
}
}
/**
* Garante que o diretório de logs existe
*/
ensureLogDirectory() {
const logDir = path.dirname(this.config.fileName);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
}
/**
* Verifica se o log deve ser processado baseado no nível
* @param {string} level - Nível do log
* @returns {boolean}
*/
shouldLog(level) {
const levelValue = this.levels[level];
return levelValue !== undefined && levelValue <= this.currentLevel;
}
/**
* Log de erro
* @param {string} message - Mensagem
* @param {Object} meta - Metadados opcionais
* @param {Error} error - Objeto de erro opcional
*/
error(message, meta = {}, error = null) {
if (!this.shouldLog('error')) return;
const logData = {
...meta,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack
} : undefined
};
this.log('error', message, logData);
}
/**
* Log de warning
* @param {string} message - Mensagem
* @param {Object} meta - Metadados opcionais
*/
warn(message, meta = {}) {
if (!this.shouldLog('warn')) return;
this.log('warn', message, meta);
}
/**
* Log de informação
* @param {string} message - Mensagem
* @param {Object} meta - Metadados opcionais
*/
info(message, meta = {}) {
if (!this.shouldLog('info')) return;
this.log('info', message, meta);
}
/**
* Log de debug
* @param {string} message - Mensagem
* @param {Object} meta - Metadados opcionais
*/
debug(message, meta = {}) {
if (!this.shouldLog('debug')) return;
this.log('debug', message, meta);
}
/**
* Log principal que processa a saída
* @param {string} level - Nível do log
* @param {string} message - Mensagem
* @param {Object} meta - Metadados
*/
log(level, message, meta = {}) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: level.toUpperCase(),
message,
service: 'tiflux-mcp',
pid: process.pid,
...meta
};
// Output para console
if (this.config.enableConsole) {
this.writeToConsole(level, logEntry);
}
// Output para arquivo
if (this.config.enableFile) {
this.writeToFile(logEntry);
}
}
/**
* Escreve log no console com formatação
* @param {string} level - Nível do log
* @param {Object} logEntry - Entrada de log
*/
writeToConsole(level, logEntry) {
if (this.config.format === 'json') {
console.error(JSON.stringify(logEntry));
} else {
// Formato texto legível
const levelColors = {
error: '\x1b[31m', // Vermelho
warn: '\x1b[33m', // Amarelo
info: '\x1b[36m', // Ciano
debug: '\x1b[90m' // Cinza
};
const reset = '\x1b[0m';
const color = levelColors[level] || '';
const metaStr = Object.keys(logEntry)
.filter(key => !['timestamp', 'level', 'message', 'service', 'pid'].includes(key))
.map(key => `${key}=${JSON.stringify(logEntry[key])}`)
.join(' ');
const output = `${color}[${logEntry.timestamp}] ${logEntry.level}${reset} ${logEntry.message}${metaStr ? ` ${metaStr}` : ''}`;
// Usar stderr para logs de erro/warn, stdout para outros
if (level === 'error' || level === 'warn') {
console.error(output);
} else {
console.log(output);
}
}
}
/**
* Escreve log no arquivo
* @param {Object} logEntry - Entrada de log
*/
writeToFile(logEntry) {
try {
// Verificar rotação de arquivo
this.rotateLogIfNeeded();
const logLine = JSON.stringify(logEntry) + '\n';
fs.appendFileSync(this.config.fileName, logLine, 'utf8');
} catch (error) {
// Fallback para console se não conseguir escrever no arquivo
console.error('Failed to write to log file:', error.message);
this.writeToConsole('error', {
...logEntry,
logFileError: error.message
});
}
}
/**
* Rotaciona arquivo de log se necessário
*/
rotateLogIfNeeded() {
try {
if (!fs.existsSync(this.config.fileName)) {
return;
}
const stats = fs.statSync(this.config.fileName);
if (stats.size < this.config.maxFileSize) {
return;
}
// Rotacionar arquivos existentes
for (let i = this.config.maxFiles - 1; i > 0; i--) {
const oldFile = `${this.config.fileName}.${i}`;
const newFile = `${this.config.fileName}.${i + 1}`;
if (fs.existsSync(oldFile)) {
if (i === this.config.maxFiles - 1) {
fs.unlinkSync(oldFile); // Deletar o mais antigo
} else {
fs.renameSync(oldFile, newFile);
}
}
}
// Mover arquivo atual para .1
fs.renameSync(this.config.fileName, `${this.config.fileName}.1`);
} catch (error) {
console.error('Failed to rotate log file:', error.message);
}
}
/**
* Cria um logger filho com contexto adicional
* @param {Object} context - Contexto a ser adicionado a todos os logs
* @returns {Logger} - Nova instância de logger
*/
child(context) {
const childLogger = new Logger(this.config);
childLogger.defaultContext = { ...this.defaultContext, ...context };
return childLogger;
}
/**
* Log de request HTTP
* @param {Object} request - Dados da requisição
* @param {number} duration - Duração em ms
* @param {number} status - Status code
*/
logRequest(request, duration, status) {
const level = status >= 400 ? 'warn' : 'info';
this.log(level, 'HTTP Request', {
method: request.method,
url: request.url,
duration: `${duration}ms`,
status,
userAgent: request.headers?.['user-agent'],
ip: request.ip || request.connection?.remoteAddress
});
}
/**
* Log de performance
* @param {string} operation - Nome da operação
* @param {number} duration - Duração em ms
* @param {Object} meta - Metadados opcionais
*/
logPerformance(operation, duration, meta = {}) {
const level = duration > 1000 ? 'warn' : 'info';
this.log(level, `Performance: ${operation}`, {
operation,
duration: `${duration}ms`,
...meta
});
}
/**
* Log de início de operação
* @param {string} operation - Nome da operação
* @param {Object} meta - Metadados opcionais
* @returns {Function} - Função para finalizar o log
*/
startTimer(operation, meta = {}) {
const startTime = Date.now();
this.debug(`Starting: ${operation}`, meta);
return (additionalMeta = {}) => {
const duration = Date.now() - startTime;
this.logPerformance(operation, duration, { ...meta, ...additionalMeta });
return duration;
};
}
/**
* Define novo nível de log
* @param {string} level - Novo nível
*/
setLevel(level) {
if (this.levels[level] !== undefined) {
this.config.level = level;
this.currentLevel = this.levels[level];
}
}
/**
* Obtém estatísticas do logger
* @returns {Object} - Estatísticas
*/
getStats() {
let fileSize = 0;
if (this.config.enableFile && fs.existsSync(this.config.fileName)) {
try {
fileSize = fs.statSync(this.config.fileName).size;
} catch (error) {
// Ignora erro
}
}
return {
level: this.config.level,
enableConsole: this.config.enableConsole,
enableFile: this.config.enableFile,
fileName: this.config.fileName,
fileSize,
maxFileSize: this.config.maxFileSize
};
}
}
module.exports = Logger;