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
JavaScript
"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;