azify-logger
Version:
Azify Logger Client - Centralized logging for OpenSearch
331 lines (280 loc) • 9.46 kB
JavaScript
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)
})