UNPKG

unnbound-logger-sdk

Version:

A structured logging library with TypeScript support using Pino. Provides consistent, well-typed logging with automatic logId, workflowId, traceId, and deploymentId tracking across operational contexts.

471 lines (470 loc) 20.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnnboundLogger = void 0; /** * Core logger implementation */ const pino_1 = __importDefault(require("pino")); const logger_utils_1 = require("./utils/logger-utils"); const uuid_1 = require("uuid"); const trace_context_1 = require("./utils/trace-context"); const axios_1 = require("axios"); const http_status_messages_1 = require("./utils/http-status-messages"); /** * UnnboundLogger provides typed, structured logging using Pino */ class UnnboundLogger { /** * Creates a new UnnboundLogger instance * @param options - Configuration options for the logger */ constructor(options = {}) { // Trace middleware this.traceMiddleware = (req, res, next) => { // Check if the route should be ignored if (this.shouldIgnorePath(req.path, this.ignoreTraceRoutes)) { return next(); } const traceId = req.header(this.traceHeaderKey) || (0, uuid_1.v4)(); res.setHeader(this.traceHeaderKey, traceId); trace_context_1.traceContext.run(traceId, () => { // Log the incoming request const reqLog = this.httpRequest(req, { traceId }); // Capture response body for logging const originalSend = res.send; res.send = function (body) { res.locals.body = body; return originalSend.call(this, body); }; // Log the response when it finishes res.on('finish', () => { this.httpResponse(res, req, { requestId: reqLog.requestId, traceId }); }); next(); }); }; // Axios trace middleware this.axiosTraceMiddleware = { onFulfilled: (config) => { // Check if the URL should be ignored if (config.url && this.shouldIgnorePath(config.url, this.ignoreAxiosTraceRoutes)) { return config; } const traceId = trace_context_1.traceContext.getTraceId(); if (traceId) { const headers = new axios_1.AxiosHeaders(config.headers); headers.set(this.traceHeaderKey, traceId); config.headers = headers; } // Store request start time and generate requestId for duration calculation and correlation const startTime = Date.now(); const requestId = (0, uuid_1.v4)(); config.metadata = { startTime, requestId }; // Log the outgoing request using proper httpRequest method const mockReq = { method: config.method?.toUpperCase() || 'UNKNOWN', url: `${config.baseURL || ''}${config.url}`, originalUrl: `${config.baseURL || ''}${config.url}`, headers: config.headers || {}, body: (0, logger_utils_1.safeJsonParse)(config.data), ip: 'outgoing', // Mark as outgoing request protocol: 'https', // Default for outgoing secure: true, get: () => undefined // Mock get method for constructFullUrl }; this.httpRequest(mockReq, { traceId, requestId, startTime }); return config; }, onRejected: (error) => { return Promise.reject(error); } }; // Axios response interceptor (should be used separately) this.axiosResponseInterceptor = { onFulfilled: (response) => { // Check if the URL should be ignored const url = `${response.config?.baseURL || ''}${response.config?.url}`; if (url && this.shouldIgnorePath(url, this.ignoreAxiosTraceRoutes)) { return response; } // Calculate duration const startTime = response.config.metadata?.startTime || Date.now(); const requestId = response.config.metadata?.requestId; const duration = Date.now() - startTime; // Log the successful response using proper httpResponse method const mockReq = { method: response.config.method?.toUpperCase() || 'UNKNOWN', url: `${response.config.baseURL || ''}${response.config.url}`, originalUrl: `${response.config.baseURL || ''}${response.config.url}`, ip: 'outgoing', // Mark as outgoing request protocol: 'https', // Default for outgoing secure: true, get: () => undefined // Mock get method for constructFullUrl }; const mockRes = { statusCode: response.status, locals: { body: (0, logger_utils_1.safeJsonParse)(response.data), startTime: startTime, traceId: trace_context_1.traceContext.getTraceId(), requestId: requestId }, getHeaders: () => response.headers || {} }; this.httpResponse(mockRes, mockReq, { requestId, duration, traceId: trace_context_1.traceContext.getTraceId() }); return response; }, onRejected: (error) => { // Check if the URL should be ignored const url = `${error.config?.baseURL || ''}${error.config?.url}`; if (url && this.shouldIgnorePath(url, this.ignoreAxiosTraceRoutes)) { return Promise.reject(error); } // Calculate duration for error responses const startTime = error.config?.metadata?.startTime || Date.now(); const requestId = error.config?.metadata?.requestId; const duration = Date.now() - startTime; // Log the error response using proper httpResponse method if (error.response) { const mockReq = { method: error.config?.method?.toUpperCase() || 'UNKNOWN', url: `${error.config?.baseURL || ''}${error.config?.url}`, originalUrl: `${error.config?.baseURL || ''}${error.config?.url}`, ip: 'outgoing', // Mark as outgoing request protocol: 'https', // Default for outgoing secure: true, get: () => undefined // Mock get method for constructFullUrl }; const mockRes = { statusCode: error.response.status, locals: { body: (0, logger_utils_1.safeJsonParse)(error.response.data), startTime: startTime, traceId: trace_context_1.traceContext.getTraceId(), requestId: requestId }, getHeaders: () => error.response.headers || {} }; this.httpResponse(mockRes, mockReq, { requestId, duration, traceId: trace_context_1.traceContext.getTraceId() }); } else { // For network errors without response, log as general error this.error('HTTP Request Failed', { context: { reason: 'No response received', method: error.config?.method?.toUpperCase() || 'UNKNOWN', url: `${error.config?.baseURL || ''}${error.config?.url}`, }, error: error.message, requestId, traceId: trace_context_1.traceContext.getTraceId() }); } return Promise.reject(error); } }; this.workflowId = process.env.UNNBOUND_WORKFLOW_ID || ''; this.workflowUrl = process.env.UNNBOUND_WORKFLOW_URL || ''; this.serviceId = process.env.UNNBOUND_SERVICE_ID || ''; this.deploymentId = process.env.UNNBOUND_DEPLOYMENT_ID || ''; this.traceHeaderKey = options.traceHeaderKey || 'unnbound-trace-id'; this.ignoreTraceRoutes = options.ignoreTraceRoutes || []; this.ignoreAxiosTraceRoutes = options.ignoreAxiosTraceRoutes || []; // Create Pino logger this.logger = (0, pino_1.default)({ level: 'info', base: {}, // Disable all default base fields (pid, hostname) timestamp: false, // Let CloudWatch handle timestamps messageKey: 'messages', // Change message field from 'msg' to 'messages' formatters: { level: (label) => { return { level: label }; }, }, }); } /** * Checks if a path matches any of the ignore patterns * @param path - The path to check * @param patterns - Array of glob patterns to match against * @returns boolean indicating if the path should be ignored */ shouldIgnorePath(path, patterns) { return patterns.some(pattern => { // Convert glob pattern to regex const regexPattern = pattern .replace(/\./g, '\\.') // Escape dots .replace(/\*/g, '.*') // Convert * to .* .replace(/\?/g, '.'); // Convert ? to . const regex = new RegExp(`^${regexPattern}$`); return regex.test(path); }); } /** * Logs a general message * @param level - Log level * @param message - Log message * @param options - Additional logging options */ log(level, message, options = {}) { const logId = (0, uuid_1.v4)(); const traceId = options.traceId || trace_context_1.traceContext.getTraceId() || (0, uuid_1.v4)(); const requestId = options.requestId || (0, uuid_1.v4)(); let logEntry; const { traceId: optionTraceId, requestId: optionRequestId, level: optionLevel, ...restOptions } = options; const baseEntry = { logId, type: 'general', workflowId: this.workflowId, serviceId: this.serviceId, traceId, requestId, deploymentId: this.deploymentId, }; if (message instanceof Error) { const error = { name: message.name, message: message.message, stack: message.stack, }; logEntry = { ...baseEntry, message: message.name, error, ...restOptions, }; } else if (typeof message === 'string') { logEntry = { ...baseEntry, message, ...restOptions, }; } else { // If message is an object, it's part of the log entry logEntry = { ...baseEntry, ...message, message: message.message || 'Structured log data', ...restOptions, }; } // Separate the message from the log data and explicitly exclude any level field const { message: logMessage, level: excludedLevel, ...logData } = logEntry; this.logger[level](logData, logMessage); return logEntry; } /** * Logs an error message * @param message - Error message or object * @param options - Additional logging options */ error(message, options = {}) { return this.log('error', message, options); } /** * Logs a warning message * @param message - Warning message * @param options - Additional logging options */ warn(message, options = {}) { return this.log('warn', message, options); } /** * Logs an info message * @param message - Info message * @param options - Additional logging options */ info(message, options = {}) { return this.log('info', message, options); } /** * Logs a debug message * @param message - Debug message * @param options - Additional logging options */ debug(message, options = {}) { return this.log('debug', message, options); } /** * Constructs the full URL from request information * @param req - Express request object * @returns Full URL string */ constructFullUrl(req) { // If the URL is already absolute, return it directly const reqUrl = req.originalUrl || req.url; if (reqUrl?.startsWith('http://') || reqUrl?.startsWith('https://')) { return reqUrl; } // Check if we have a workflow URL configured (preferred method) if (this.workflowUrl) { // Use workflow URL as the base URL return `${this.workflowUrl.replace(/\/$/, '')}${reqUrl}`; } // Fallback to constructing from request info for incoming requests const protocol = req.protocol || (req.secure ? 'https' : 'http'); const host = req.get('host') || req.get('x-forwarded-host') || 'localhost'; return `${protocol}://${host}${reqUrl}`; } /** * Logs an HTTP request * @param req - Express request object * @param options - Additional logging options * @returns The request ID for correlating with the response */ httpRequest(req, options = {}) { const logId = (0, uuid_1.v4)(); const traceId = options.traceId || trace_context_1.traceContext.getTraceId() || (0, uuid_1.v4)(); const requestId = options.requestId || (0, uuid_1.v4)(); const startTime = options.startTime || Date.now(); // Store request metadata in res.locals for later use if (req.res) { req.res.locals.requestId = requestId; req.res.locals.startTime = startTime; req.res.locals.traceId = traceId; req.res.locals.workflowId = this.workflowId; req.res.locals.workflowUrl = this.workflowUrl; req.res.locals.serviceId = this.serviceId; } const logEntry = { logId, type: 'httpRequest', message: req.ip === 'outgoing' ? 'Outgoing HTTP Request' : 'Incoming HTTP Request', workflowId: this.workflowId, serviceId: this.serviceId, traceId, requestId, deploymentId: this.deploymentId, duration: 0, // Will be updated in response httpRequest: { url: this.constructFullUrl(req), method: req.method, headers: (0, logger_utils_1.filterHeaders)(req.headers), ip: (0, logger_utils_1.normalizeIp)(req.ip), body: (0, logger_utils_1.safeJsonParse)(req.body), }, }; const { message: logMessage, ...logData } = logEntry; this.logger[options.level || 'info'](logData, logMessage); return logEntry; } /** * Logs an HTTP response * @param res - Express response object * @param req - Express request object * @param options - Additional logging options */ httpResponse(res, req, options = {}) { const logId = (0, uuid_1.v4)(); const requestId = res.locals.requestId || options.requestId || (0, uuid_1.v4)(); const startTime = res.locals.startTime || options.startTime || Date.now(); const workflowId = res.locals.workflowId || this.workflowId; const serviceId = res.locals.serviceId || this.serviceId; const traceId = res.locals.traceId || options.traceId || trace_context_1.traceContext.getTraceId() || (0, uuid_1.v4)(); const duration = options.duration || (Date.now() - startTime); // Determine log level based on status code let level = options.level || 'info'; if (!options.level) { if (res.statusCode >= 400) { level = 'error'; } else { level = 'info'; } } const logEntry = { logId, type: 'httpResponse', message: (0, http_status_messages_1.getStatusMessage)(res.statusCode), workflowId: workflowId || '', serviceId: serviceId || '', traceId, requestId, deploymentId: this.deploymentId, duration, httpResponse: { url: this.constructFullUrl(req), method: req.method, headers: (0, logger_utils_1.filterHeaders)(res.getHeaders()), ip: (0, logger_utils_1.normalizeIp)(req.ip), status: res.statusCode, body: (0, logger_utils_1.safeJsonParse)(res.locals.body), }, }; const { message: logMessage, ...logData } = logEntry; this.logger[level](logData, logMessage); return logEntry; } /** * Logs an SFTP transaction * @param operation - SFTP operation details * @param options - Additional logging options */ sftpTransaction(operation, options = {}) { const logId = (0, uuid_1.v4)(); const traceId = options.traceId || trace_context_1.traceContext.getTraceId() || (0, uuid_1.v4)(); const requestId = options.requestId || (0, uuid_1.v4)(); const duration = options.duration || (options.startTime ? Date.now() - options.startTime : 0); const level = operation.status === 'success' ? 'info' : 'error'; const logEntry = { logId, type: 'sftpTransaction', message: `SFTP ${operation.operation} ${operation.status} - ${operation.path}`, workflowId: this.workflowId, serviceId: this.serviceId, traceId, requestId, deploymentId: this.deploymentId, duration, sftp: operation, }; const { message: logMessage, ...logData } = logEntry; this.logger[level](logData, logMessage); return logEntry; } /** * Logs a database query transaction * @param query - Database query details * @param options - Additional logging options */ dbQueryTransaction(query, options = {}) { const logId = (0, uuid_1.v4)(); const traceId = options.traceId || trace_context_1.traceContext.getTraceId() || (0, uuid_1.v4)(); const requestId = options.requestId || (0, uuid_1.v4)(); const duration = options.duration || (options.startTime ? Date.now() - options.startTime : 0); const level = query.status === 'success' ? 'info' : 'error'; const logEntry = { logId, type: 'dbQueryTransaction', message: `DB Query ${query.status} - ${query.vendor}`, workflowId: this.workflowId, serviceId: this.serviceId, traceId, requestId, deploymentId: this.deploymentId, duration, db: query, }; const { message: logMessage, ...logData } = logEntry; this.logger[level](logData, logMessage); return logEntry; } } exports.UnnboundLogger = UnnboundLogger;