azify-logger
Version:
Azify Logger Client - Centralized logging for OpenSearch
425 lines (371 loc) • 14.1 kB
JavaScript
const axios = require('axios')
const { als, runWithRequestContext, startRequestContext, getRequestContext } = require('./store')
/**
* Creates an Express middleware for automatic request/response logging with azify-logger
* @param {Object} options - Configuration options
* @param {string} [options.serviceName] - Name of the service (defaults to APP_NAME env var or 'assemble')
* @param {string} [options.loggerUrl] - URL of the azify-logger service (defaults to AZIFY_LOGGER_URL env var or 'http://localhost:3001')
* @param {string} [options.environment] - Environment name (defaults to NODE_ENV env var or 'development')
* @returns {Function} Express middleware function
* @example
* const azifyMiddleware = require('azify-logger/middleware-express');
* app.use(azifyMiddleware({ serviceName: 'my-app' }));
*/
function createExpressLoggingMiddleware(options = {}) {
const config = {
serviceName: options.serviceName || process.env.APP_NAME || 'assemble',
loggerUrl: options.loggerUrl || process.env.AZIFY_LOGGER_URL || 'http://localhost:3001',
environment: options.environment || process.env.NODE_ENV || 'development'
}
/**
* Sends a log entry to the azify-logger service
* @param {string} level - Log level (info, error, warn, debug)
* @param {string} message - Log message
* @param {Object} [meta={}] - Additional metadata to include in the log
* @private
*/
async function sendLog(level, message, meta = {}) {
const logData = {
level,
message,
meta: {
...meta,
service: {
name: config.serviceName,
version: '1.0.0'
},
environment: config.environment,
timestamp: new Date().toISOString(),
hostname: require('os').hostname()
}
}
try {
await axios.post(`${config.loggerUrl}`, logData, {
timeout: 5000
})
} catch (error) {
console.error('Erro ao enviar log:', error.message)
}
}
/**
* Express middleware function that logs requests and responses
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
* @returns {void}
*/
return function azifyExpressLoggingMiddleware(req, res, next) {
const startTime = Date.now()
const requestId = req.requestId || require('uuid').v4()
const middlewareId = require('uuid').v4().substring(0, 8)
let existingTraceId = null
let existingSpanId = null
let existingParentSpanId = null
if (req.headers['x-trace-id']) {
existingTraceId = req.headers['x-trace-id']
existingSpanId = req.headers['x-span-id']
existingParentSpanId = req.headers['x-parent-span-id']
}
const currentCtx = getRequestContext()
if (currentCtx && currentCtx.traceId) {
existingTraceId = currentCtx.traceId
existingSpanId = currentCtx.spanId
existingParentSpanId = currentCtx.parentSpanId
}
let reqCtx
if (existingTraceId) {
reqCtx = {
traceId: existingTraceId,
spanId: existingSpanId || require('uuid').v4().substring(0, 16),
parentSpanId: existingParentSpanId,
requestId: requestId
}
} else {
reqCtx = startRequestContext({ requestId })
}
const requestTraceId = reqCtx.traceId
const requestSpanId = reqCtx.spanId
const requestParentSpanId = reqCtx.parentSpanId
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error
}
console.log = (...args) => {
const message = args.map(String).join(' ')
sendLog('info', message, {
traceId: requestTraceId,
spanId: requestSpanId,
parentSpanId: requestParentSpanId,
requestId: requestId
})
}
console.info = (...args) => {
const message = args.map(String).join(' ')
sendLog('info', message, {
traceId: requestTraceId,
spanId: requestSpanId,
parentSpanId: requestParentSpanId,
requestId: requestId
})
}
console.warn = (...args) => {
const message = args.map(String).join(' ')
sendLog('warn', message, {
traceId: requestTraceId,
spanId: requestSpanId,
parentSpanId: requestParentSpanId,
requestId: requestId
})
}
console.error = (...args) => {
const message = args.map(String).join(' ')
sendLog('error', message, {
traceId: requestTraceId,
spanId: requestSpanId,
parentSpanId: requestParentSpanId,
requestId: requestId
})
}
let baseUrl = req.url
if (baseUrl.includes('?')) {
baseUrl = baseUrl.substring(0, baseUrl.indexOf('?'))
}
baseUrl = baseUrl.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/{id}')
baseUrl = baseUrl.replace(/\/[0-9]+/g, '/{id}')
function sanitizeHeaders(headers) {
const sanitized = { ...headers }
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'x-access-token']
for (const key of Object.keys(sanitized)) {
if (sensitiveHeaders.includes(key.toLowerCase())) {
sanitized[key] = '***'
}
}
return sanitized
}
function sanitizeBody(body) {
if (!body || typeof body !== 'object') return body
const sanitized = Array.isArray(body) ? [...body] : { ...body }
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'api_key', 'accessToken', 'access_token', 'refreshToken', 'refresh_token', 'clientSecret', 'client_secret']
for (const key of Object.keys(sanitized)) {
if (sensitiveFields.includes(key) || key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
sanitized[key] = '***'
}
}
return sanitized
}
req._azifyRequestData = {
requestId,
method: req.method,
url: req.url,
baseUrl: baseUrl,
path: req.url,
headers: sanitizeHeaders(req.headers || {}),
query: req.query || {},
userAgent: (req.headers && req.headers['user-agent']) || 'unknown',
ip: (req.connection && req.connection.remoteAddress) || (req.socket && req.socket.remoteAddress) || req.ip || 'unknown',
traceId: requestTraceId,
spanId: requestSpanId,
parentSpanId: requestParentSpanId
}
if (req.method === 'GET') {
sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
} else {
if (req.body !== undefined) {
req._azifyRequestData.requestBody = sanitizeBody(req.body)
}
sendLog('info', `[REQUEST] ${req.method} ${req.url}`, req._azifyRequestData)
}
res.on('finish', () => {
if (!responseLogged) {
logResponse()
responseLogged = true
}
console.log = originalConsole.log
console.info = originalConsole.info
console.warn = originalConsole.warn
console.error = originalConsole.error
})
res.on('close', () => {
if (!responseLogged) {
logResponse()
responseLogged = true
}
console.log = originalConsole.log
console.info = originalConsole.info
console.warn = originalConsole.warn
console.error = originalConsole.error
})
let sentBody
let responseLogged = false
const originalSend = res.send && res.send.bind(res)
if (originalSend) {
res.send = function patchedSend() {
try {
if (arguments.length === 1) {
sentBody = arguments[0]
} else if (arguments.length >= 2) {
sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
}
} catch (_) {}
if (!responseLogged) {
logResponse()
responseLogged = true
}
return originalSend.apply(this, arguments)
}
}
const originalStatus = res.status
res.status = function(code) {
res._actualStatusCode = code
return originalStatus.call(this, code)
}
const originalWriteHead = res.writeHead
res.writeHead = function(statusCode, statusMessage, headers) {
res._actualStatusCode = statusCode
if (typeof statusMessage === 'object') {
headers = statusMessage
statusMessage = undefined
}
return originalWriteHead.call(this, statusCode, statusMessage, headers)
}
const originalJsonMethod = res.json
res.json = function(code, body) {
try {
if (arguments.length === 1) {
sentBody = arguments[0]
} else if (arguments.length >= 2) {
sentBody = typeof arguments[0] === 'number' ? arguments[1] : (arguments[1] || arguments[0])
}
} catch (_) {}
if (typeof code === 'number') {
res._actualStatusCode = code
} else {
const errorObj = arguments.length === 1 ? arguments[0] : (typeof arguments[0] === 'number' ? arguments[1] : arguments[0])
if (errorObj && errorObj.constructor && errorObj.constructor.name === 'ErrCtor') {
const errorName = errorObj.toString()
if (errorName.includes('InternalServerError') || errorName.includes('InternalError')) {
res._actualStatusCode = 500
} else if (errorName.includes('BadRequest') || errorName.includes('BadDigest')) {
res._actualStatusCode = 400
} else if (errorName.includes('NotFound')) {
res._actualStatusCode = 404
} else if (errorName.includes('Unauthorized')) {
res._actualStatusCode = 401
} else if (errorName.includes('Forbidden')) {
res._actualStatusCode = 403
} else {
res._actualStatusCode = 500
}
} else {
res._actualStatusCode = res.statusCode || 200
}
}
if (!responseLogged) {
logResponse()
responseLogged = true
}
return originalJsonMethod.apply(this, arguments)
}
res.on('finish', function() {
if (!responseLogged) {
logResponse()
responseLogged = true
}
})
const originalEnd = res.end
res.end = function(chunk, encoding) {
const duration = Date.now() - startTime
let responseBody = sentBody
try {
if (responseBody == null && chunk) {
if (Buffer.isBuffer(chunk)) {
responseBody = chunk.toString('utf8')
} else if (typeof chunk === 'string') {
responseBody = chunk
} else {
responseBody = JSON.stringify(chunk)
}
}
} catch (_) {}
if (!responseLogged) {
logResponse()
responseLogged = true
}
originalEnd.call(this, chunk, encoding)
}
/**
* Logs the response data to azify-logger
* @private
*/
function logResponse() {
const duration = Date.now() - startTime
let responseBody = sentBody
let serializedResponseBody
try {
if (typeof responseBody === 'string') {
serializedResponseBody = responseBody
} else if (Array.isArray(responseBody)) {
serializedResponseBody = JSON.stringify(responseBody)
} else if (responseBody && typeof responseBody === 'object') {
if (responseBody.toJSON && typeof responseBody.toJSON === 'function') {
serializedResponseBody = JSON.stringify(responseBody.toJSON())
} else if (responseBody.toString && typeof responseBody.toString === 'function' && responseBody.toString() !== '[object Object]') {
serializedResponseBody = responseBody.toString()
} else {
serializedResponseBody = JSON.stringify(responseBody, (key, value) => {
if (typeof value === 'function') {
return '[Function]'
}
if (value instanceof Error) {
return { name: value.name, message: value.message, stack: value.stack }
}
return value
}, null, 0)
}
} else {
serializedResponseBody = responseBody != null ? String(responseBody) : ''
}
} catch (error) {
try {
serializedResponseBody = JSON.stringify(responseBody, null, 2)
} catch (secondError) {
serializedResponseBody = '[Complex object - serialization failed]'
}
}
const statusCode = res._actualStatusCode || res._statusCode || res.statusCode || 200
let parsedResponseBody = serializedResponseBody
try {
if (typeof serializedResponseBody === 'string' && serializedResponseBody.trim().startsWith('{')) {
parsedResponseBody = JSON.parse(serializedResponseBody)
} else if (typeof serializedResponseBody === 'string' && serializedResponseBody.trim().startsWith('[')) {
parsedResponseBody = JSON.parse(serializedResponseBody)
}
} catch (_) {}
const responseMessage = serializedResponseBody && serializedResponseBody.length > 0
? `[RESPONSE] ${req.method} ${req.url} ${statusCode}`
: `[RESPONSE] ${req.method} ${req.url} ${statusCode} ${duration}ms`
const responseData = {
...req._azifyRequestData,
requestBody: req.body,
statusCode: statusCode,
responseTime: duration,
responseHeaders: res.getHeaders ? res.getHeaders() : {},
responseBody: parsedResponseBody // Use parsed object instead of string
}
try { res._azifyResponseLogged = true } catch (_) {}
sendLog('info', responseMessage, responseData)
}
req._azifyContext = reqCtx
try {
runWithRequestContext(reqCtx, () => {
next()
})
} catch (error) {
console.error('Error in azify middleware:', error)
next()
}
}
}
module.exports = createExpressLoggingMiddleware