UNPKG

azify-logger

Version:

Azify Logger Client - Centralized logging for OpenSearch

425 lines (371 loc) 14.1 kB
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