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
JavaScript
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;