UNPKG

azify-logger

Version:

Azify Logger Client - Centralized logging for OpenSearch

331 lines (280 loc) 9.46 kB
require('./register-otel.js') const axios = require('axios') const express = require('express') const cors = require('cors') const os = require('os') let trace, context, propagation, W3CTraceContextPropagator try { const otelApi = require('@opentelemetry/api') const otelCore = require('@opentelemetry/core') trace = otelApi.trace context = otelApi.context propagation = otelApi.propagation W3CTraceContextPropagator = otelCore.W3CTraceContextPropagator } catch (_) { trace = { getSpan: () => null } context = { active: () => ({}) } propagation = { extract: () => ({}), inject: () => {} } W3CTraceContextPropagator = class {} } const app = express() app.set('trust proxy', 1) app.use(express.json({ limit: '10mb' })) app.use(express.urlencoded({ extended: true, limit: '10mb' })) app.use(cors()) const IS_LOCAL = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev' || !process.env.NODE_ENV function isPrivateOrLocalhost(ip) { if (IS_LOCAL) { return true } ip = ip.replace('::ffff:', '').replace('::1', '127.0.0.1') if (ip === '127.0.0.1' || ip === 'localhost' || ip === '::1') { return true } const parts = ip.split('.').map(Number) if (parts.length !== 4) { return false } if (parts[0] === 10) return true if (parts[0] === 127) return true if (parts[0] === 192 && parts[1] === 168) return true if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true return false } function validateNetworkAccess(req, res, next) { if (req.path === '/health' || req.path === '/') { return next() } const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.headers['x-real-ip'] || req.ip || req.connection.remoteAddress || req.socket.remoteAddress if (!IS_LOCAL && !isPrivateOrLocalhost(clientIP)) { return res.status(403).json({ success: false, message: 'Forbidden. Access denied - endpoint only accessible from same server (localhost/private IPs).', clientIP: clientIP }) } next() } app.use(validateNetworkAccess) const tracer = trace.getTracer('azify-logger', '1.0.0') const propagator = new W3CTraceContextPropagator() const traceContextMap = new Map() /** * Creates an index template for dynamic log indices * This allows OpenSearch to auto-create indices like logs-azipay, logs-assemble, etc. * @returns {Promise<void>} * @private */ async function ensureIndexTemplate() { const templateName = 'logs-template' const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200' try { await axios.put(`${osUrl}/_index_template/${templateName}`, { index_patterns: ['logs-*'], template: { settings: { number_of_shards: 1, number_of_replicas: 0, 'index.refresh_interval': '5s' }, mappings: { properties: { '@timestamp': { type: 'date' }, level: { type: 'keyword' }, message: { type: 'text' }, service: { properties: { name: { type: 'keyword' }, version: { type: 'keyword' } } }, appName: { type: 'keyword' }, traceId: { type: 'keyword' }, spanId: { type: 'keyword' }, parentSpanId: { type: 'keyword' }, userId: { type: 'keyword' }, requestId: { type: 'keyword' }, method: { type: 'keyword' }, url: { type: 'keyword' }, statusCode: { type: 'integer' }, responseTime: { type: 'float' }, ip: { type: 'ip' }, userAgent: { type: 'text' }, environment: { type: 'keyword' }, hostname: { type: 'keyword' }, responseBody: { type: 'text' }, error: { properties: { message: { type: 'text' }, stack: { type: 'text' }, name: { type: 'keyword' } } } } } }, priority: 500 }) console.log(`✅ Index template ${templateName} criado/atualizado no OpenSearch`) console.log(` Índices serão criados automaticamente no formato: logs-{service-name}`) } catch (error) { console.error('❌ Erro ao criar index template:', error.message) if (error.response) { console.error(' Detalhes:', error.response.data) } } } ensureIndexTemplate() function generateTraceId() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) } function generateSpanId() { return Math.random().toString(36).substring(2, 15) } function getOrCreateTraceContext(requestId) { if (traceContextMap.has(requestId)) { return traceContextMap.get(requestId) } const traceContext = { traceId: generateTraceId(), spanId: generateSpanId(), parentSpanId: null } traceContextMap.set(requestId, traceContext) setTimeout(() => { traceContextMap.delete(requestId) }, 30000) return traceContext } app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'azify-logger' }) }) app.get('/', (req, res) => { res.json({ service: 'azify-logger', version: '1.0.0', endpoints: { health: '/health', testLog: '/test-log' } }) }) app.post('/log', async (req, res) => { let { level, message, meta } = req.body if (!level || !message) { return res.status(400).json({ success: false, message: 'Level and message are required.' }) } const shouldFilterLog = ( message.includes('prisma:query') || message.includes('prisma:info') || message.includes('Starting a mysql pool') || message.includes('SELECT `') || message.includes('INSERT `') || message.includes('UPDATE `') || message.includes('DELETE `') || message.includes('%s: %s') || message.includes('Application Startup Time') ) if (shouldFilterLog) { return res.json({ success: true, message: 'Log filtrado (Prisma verboso)' }) } const requestId = meta && meta.requestId let traceContext = null if (meta && meta.traceId && meta.spanId) { traceContext = { traceId: meta.traceId, spanId: meta.spanId, parentSpanId: meta.parentSpanId || null } } else { try { const extractedCtx = propagation.extract(context.active(), req.headers, { get (carrier, key) { const header = carrier[key] if (Array.isArray(header)) return header[0] return header }, keys (carrier) { return Object.keys(carrier) } }) const span = trace.getSpan(extractedCtx) const spanContext = (span && span.spanContext && span.spanContext()) || null if (spanContext && spanContext.traceId && spanContext.spanId) { traceContext = { traceId: spanContext.traceId, spanId: spanContext.spanId, parentSpanId: null } } } catch (_) {} } if (!traceContext && requestId) { traceContext = getOrCreateTraceContext(requestId) } if (!traceContext) { traceContext = { traceId: generateTraceId(), spanId: generateSpanId(), parentSpanId: null } } const logEntry = { '@timestamp': (meta && meta.timestamp) || new Date().toISOString(), level, message, service: { name: (meta && meta.service && meta.service.name) || 'unknown-service', version: (meta && meta.service && meta.service.version) || '1.0.0' }, appName: (meta && meta.appName) || (meta && meta.service && meta.service.name) || undefined, environment: (meta && meta.environment) || process.env.NODE_ENV || 'development', hostname: (meta && meta.hostname) || os.hostname(), traceId: traceContext.traceId, spanId: traceContext.spanId, parentSpanId: traceContext.parentSpanId } if (meta) { Object.keys(meta).forEach(key => { if (!['timestamp', 'service', 'environment', 'hostname', 'traceId', 'spanId', 'parentSpanId'].includes(key)) { logEntry[key] = meta[key] } }) } try { const osUrl = process.env.OPENSEARCH_URL || 'http://localhost:9200' const serviceName = (logEntry.service.name || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '-') const indexName = `logs-${serviceName}` await axios.post(`${osUrl}/${indexName}/_doc`, logEntry, { headers: { 'Content-Type': 'application/json' } }) console.log(`✅ [${level.toUpperCase()}] ${message} | traceId: ${traceContext.traceId.substring(0, 8)}... | service: ${logEntry.service.name} | index: ${indexName}`) res.json({ success: true, message: 'Log enviado com sucesso', index: indexName }) } catch (error) { console.error('❌ Erro ao enviar log para OpenSearch:', error.message) if (error.response) { console.error(' Detalhes:', error.response.data) } res.status(500).json({ success: false, message: 'Erro ao enviar log para OpenSearch' }) } }) const port = process.env.PORT || 3001 app.listen(port, () => { console.log(`🚀 Azify Logger rodando na porta ${port}`) }) process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down') process.exit(0) }) process.on('SIGINT', () => { console.log('Received SIGINT, shutting down') process.exit(0) })