UNPKG

api-stats-logger

Version:

SDK completo de logging e monitoramento de APIs com nova estrutura de logs organizada, auto-instrumentação, dashboard em tempo real e CLI para configuração automática. Suporta logs estruturados por contexto (HTTP, business, security, system) com campos op

481 lines (412 loc) 13.6 kB
const axios = require('axios'); const performanceNow = require('performance-now'); const os = require('os'); class Logger { /** * @param {Object} options * @param {string} options.apiKey - Sua API Key * @param {string} options.url - URL do endpoint de logs * @param {string} [options.service] - Nome do serviço * @param {string} [options.environment] - Ambiente (production, staging, etc) * @param {number} [options.batchSize=20] - Tamanho do lote para envio (aumentado) * @param {number} [options.flushInterval=5000] - Intervalo de flush em ms (aumentado) * @param {boolean} [options.enabled=true] - Se o logger está habilitado * @param {number} [options.maxRetries=3] - Máximo de tentativas de reenvio * @param {number} [options.retryDelay=2000] - Delay entre tentativas (ms) (aumentado) * @param {boolean} [options.enrichLogs=true] - Se deve enriquecer logs com dados do sistema * @param {boolean} [options.autoCategory=true] - Se deve categorizar logs automaticamente */ constructor({ apiKey, url, service, environment, batchSize = 20, // Aumentado de 10 para 20 flushInterval = 5000, // Aumentado de 2000 para 5000ms enabled = true, maxRetries = 3, retryDelay = 2000, // Aumentado de 1000 para 2000ms enrichLogs = true, autoCategory = true }) { this.apiKey = apiKey; this.url = url; this.service = service; this.environment = environment; this.batchSize = batchSize; this.flushInterval = flushInterval; this.enabled = enabled; this.maxRetries = maxRetries; this.retryDelay = retryDelay; this.enrichLogs = enrichLogs; this.autoCategory = autoCategory; this.buffer = []; this.failedBuffer = []; // Buffer para logs que falharam this.stats = { sent: 0, failed: 0, retries: 0, lastFlush: null, rateLimitErrors: 0 }; // Informações do sistema para enriquecimento this.systemInfo = this._getSystemInfo(); this.sdkInfo = { name: 'api-stats-logger', version: '1.0.0', framework: this._detectFramework() }; if (this.enabled) { this.timer = setInterval(() => this.flush(), this.flushInterval); // Timer para retry de logs que falharam (com delay maior) this.retryTimer = setInterval(() => this.retryFailed(), this.retryDelay * 3); } } /** * Método principal para logging com nova estrutura * @param {Object} logData - Dados do log no novo formato estruturado */ log(logData) { if (!this.enabled) return; // Se for um objeto simples (compatibilidade), converte para novo formato if (typeof logData === 'object' && !logData.level && !logData.message) { // Formato antigo - converter const { level, message, service, environment, metadata = {} } = logData; logData = { level: level || 'info', message: message || 'Log message', service: service || this.service, environment: environment || this.environment, metadata }; } // Estruturar o log no novo formato const structuredLog = this._structureLog(logData); this.buffer.push(structuredLog); if (this.buffer.length >= this.batchSize) { this.flush(); } } /** * Estrutura um log no novo formato organizado * @param {Object} logData - Dados do log * @returns {Object} Log estruturado */ _structureLog(logData) { const now = new Date(); // Estrutura base obrigatória const structuredLog = { timestamp: logData.timestamp || now.toISOString(), level: logData.level || 'info', message: logData.message || 'Log message', service: logData.service || this.service, environment: logData.environment || this.environment }; // Adicionar categoria automaticamente se habilitado if (this.autoCategory && !logData.category) { structuredLog.category = this._determineCategory(logData); } else if (logData.category) { structuredLog.category = logData.category; } // Adicionar contexto se fornecido if (logData.context) { structuredLog.context = this._cleanObject(logData.context); } // Adicionar dados HTTP se fornecidos if (logData.http) { structuredLog.http = this._cleanObject(logData.http); } // Adicionar contexto de negócio se fornecido if (logData.business) { structuredLog.business = this._cleanObject(logData.business); } // Adicionar informações de erro se fornecidas if (logData.error) { structuredLog.error = this._cleanObject(logData.error); } // Adicionar dados de database se fornecidos if (logData.database) { structuredLog.database = this._cleanObject(logData.database); } // Adicionar contexto de segurança se fornecido if (logData.security) { structuredLog.security = this._cleanObject(logData.security); } // Adicionar métricas se fornecidas if (logData.metrics) { structuredLog.metrics = this._cleanObject(logData.metrics); } // Adicionar enriquecimento se fornecido if (logData.enrichment) { structuredLog.enrichment = this._cleanObject(logData.enrichment); } // Enriquecer com dados do sistema se habilitado if (this.enrichLogs) { if (!structuredLog.system && !logData.system) { structuredLog.system = this.systemInfo; } else if (logData.system) { structuredLog.system = this._cleanObject(logData.system); } if (!structuredLog.sdk && !logData.sdk) { structuredLog.sdk = this.sdkInfo; } else if (logData.sdk) { structuredLog.sdk = this._cleanObject(logData.sdk); } } // Manter compatibilidade com metadata antigo if (logData.metadata) { structuredLog.metadata = this._cleanObject(logData.metadata); } return structuredLog; } /** * Determina a categoria do log automaticamente * @param {Object} logData - Dados do log * @returns {string} Categoria determinada */ _determineCategory(logData) { if (logData.http) return 'http_request'; if (logData.database) return 'database_query'; if (logData.error || logData.level === 'error') return 'error'; if (logData.business) return 'business_event'; if (logData.system) return 'system_metric'; if (logData.metadata) { const type = logData.metadata.type; if (type === 'http_response' || type === 'http_request') return 'http_request'; if (type === 'database_query') return 'database_query'; } return 'application_log'; } /** * Remove campos vazios, null ou undefined de um objeto recursivamente * @param {*} obj - Objeto a ser limpo * @returns {*} Objeto limpo */ _cleanObject(obj) { if (obj === null || obj === undefined) { return undefined; } if (Array.isArray(obj)) { const cleaned = obj.map(item => this._cleanObject(item)).filter(item => item !== undefined); return cleaned.length > 0 ? cleaned : undefined; } if (typeof obj === 'object') { const cleaned = {}; let hasValidFields = false; for (const [key, value] of Object.entries(obj)) { const cleanedValue = this._cleanObject(value); if (cleanedValue !== undefined) { cleaned[key] = cleanedValue; hasValidFields = true; } } return hasValidFields ? cleaned : undefined; } // Para strings vazias, retorna undefined if (typeof obj === 'string' && obj.trim() === '') { return undefined; } return obj; } /** * Obtém informações básicas do sistema (versão segura) * @returns {Object} Informações básicas do sistema */ _getSystemInfo() { try { return { platform: process.platform, nodeVersion: process.version, timestamp: new Date().toISOString() }; } catch (error) { return { platform: 'unknown', timestamp: new Date().toISOString() }; } } /** * Detecta o framework em uso * @returns {string} Nome do framework */ _detectFramework() { try { if (typeof require !== 'undefined') { // Tentar detectar frameworks comuns try { require.resolve('express'); return 'express'; } catch (e) {} try { require.resolve('@nestjs/core'); return 'nestjs'; } catch (e) {} try { require.resolve('fastify'); return 'fastify'; } catch (e) {} try { require.resolve('koa'); return 'koa'; } catch (e) {} } } catch (error) { // Ignore errors } return 'unknown'; } // Métodos de conveniência para diferentes níveis info(message, data = {}) { this.log({ level: 'info', message, ...data }); } warn(message, data = {}) { this.log({ level: 'warn', message, ...data }); } error(message, data = {}) { this.log({ level: 'error', message, ...data }); } debug(message, data = {}) { this.log({ level: 'debug', message, ...data }); } // Métodos específicos para contextos logHttpRequest(requestData, responseData, performanceData) { this.log({ level: responseData?.statusCode >= 400 ? 'warn' : 'info', message: `HTTP ${requestData.method} ${requestData.url} ${responseData?.statusCode || 'pending'}`, category: 'http_request', http: { request: requestData, response: responseData, performance: performanceData } }); } logBusinessEvent(operation, data = {}) { this.log({ level: 'info', message: `Business event: ${operation}`, category: 'business_event', business: { operation, ...data } }); } logDatabaseQuery(query, duration, result = {}) { this.log({ level: 'info', message: `Database query executed`, category: 'database_query', database: { query, duration, ...result } }); } logError(error, context = {}) { this.log({ level: 'error', message: error.message || 'Unknown error', category: 'error', error: { type: error.name || 'Error', message: error.message, stack: error.stack, code: error.code, resolved: false }, context }); } // Resto dos métodos existentes (flush, retryFailed, etc.) permanecem iguais async flush() { if (this.buffer.length === 0) return; const toSend = this.buffer.splice(0, this.batchSize); const startTime = performanceNow(); try { await this._sendLogs(toSend); const duration = performanceNow() - startTime; this.stats.sent += toSend.length; this.stats.lastFlush = new Date().toISOString(); // Log de métricas ocasionalmente if (this.stats.sent % 100 === 0) { this._logStats(duration); } } catch (err) { this.stats.failed += toSend.length; this.failedBuffer.push(...toSend.map(log => ({ log, attempts: 0 }))); this._handleError(err, toSend.length); } } async retryFailed() { if (this.failedBuffer.length === 0) return; const toRetry = this.failedBuffer.splice(0, Math.min(this.batchSize, this.failedBuffer.length)); const logsToSend = toRetry.map(item => item.log); try { await this._sendLogs(logsToSend); this.stats.sent += logsToSend.length; this.stats.retries += toRetry.length; } catch (err) { // Recolocar na fila apenas se não excedeu máximo de tentativas const itemsToRequeue = toRetry.filter(item => { item.attempts++; return item.attempts < this.maxRetries; }); if (itemsToRequeue.length > 0) { this.failedBuffer.unshift(...itemsToRequeue); } this._handleError(err, logsToSend.length); } } async _sendLogs(logs) { const response = await axios.post(this.url, logs, { headers: { 'x-api-key': this.apiKey, 'Content-Type': 'application/json', 'User-Agent': 'api-stats-logger/1.0.0' }, timeout: 15000, // Aumentado de 10000 para 15000ms }); if (response.status !== 200 && response.status !== 201) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } _handleError(error, logCount) { if (error.response?.status === 429) { this.stats.rateLimitErrors++; console.warn(`⚠️ Rate limit atingido. ${logCount} logs não enviados.`); } else { console.error(`❌ Erro ao enviar ${logCount} logs:`, error.message); } } _logStats(duration) { console.log(`📊 Stats: ${this.stats.sent} enviados, ${this.stats.failed} falharam, ${this.stats.retries} reenvios, última flush: ${duration.toFixed(2)}ms`); } getStats() { return { ...this.stats, bufferSize: this.buffer.length, failedBufferSize: this.failedBuffer.length, systemInfo: this.systemInfo, sdkInfo: this.sdkInfo }; } async close() { if (this.timer) { clearInterval(this.timer); this.timer = null; } if (this.retryTimer) { clearInterval(this.retryTimer); this.retryTimer = null; } await this.flush(); if (this.failedBuffer.length > 0) { await this.retryFailed(); } } } module.exports = Logger;