digital-samba-mcp-server
Version:
Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API
250 lines • 8.74 kB
JavaScript
/**
* Logger configuration for Digital Samba MCP Server
*
* This module configures a Winston logger with console and file transports.
* It provides structured logging with timestamps and supports different log levels
* that can be configured through the LOG_LEVEL environment variable.
*
* @module logger
* @author Digital Samba Team
* @version 0.1.0
*/
// Node.js built-in modules
import os from 'os';
import { randomUUID } from 'crypto';
// External dependencies
import winston from 'winston';
const { format, createLogger, transports } = winston;
// Generate an instance ID at startup for correlating logs from the same process
const instanceId = randomUUID();
/**
* Format function to add standard metadata to all log entries
*/
const addStandardMetadata = format((info) => {
// Add standard metadata to every log
return {
...info,
instanceId,
hostname: os.hostname(),
nodeVersion: process.version,
pid: process.pid,
timestamp: info.timestamp || new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
};
});
/**
* Request context storage for correlating logs within a request lifecycle
*/
export class LogContext {
/**
* Initialize a new request context with a unique ID
* @param sessionId Optional session ID
* @returns Request ID for the new context
*/
static initRequest(sessionId) {
const requestId = randomUUID();
this.requestContext.set(requestId, {
requestId,
sessionId,
startTime: Date.now()
});
return requestId;
}
/**
* Get the metadata for a request context
* @param requestId Request ID
* @returns Metadata object or empty object if not found
*/
static getContext(requestId) {
return this.requestContext.get(requestId) || {};
}
/**
* Update the metadata for a request context
* @param requestId Request ID
* @param metadata Metadata to merge with existing context
*/
static updateContext(requestId, metadata) {
const existing = this.requestContext.get(requestId) || {};
this.requestContext.set(requestId, {
...existing,
...metadata
});
}
/**
* Remove a request context when the request is complete
* @param requestId Request ID
*/
static endRequest(requestId) {
this.requestContext.delete(requestId);
}
/**
* Create a logger that includes the request context in every log entry
* @param requestId Request ID
* @returns Logger with request context
*/
static getContextLogger(requestId) {
return {
debug: (message, metadata) => {
logger.debug(message, { ...this.getContext(requestId), ...metadata });
},
info: (message, metadata) => {
logger.info(message, { ...this.getContext(requestId), ...metadata });
},
warn: (message, metadata) => {
logger.warn(message, { ...this.getContext(requestId), ...metadata });
},
error: (message, metadata) => {
logger.error(message, { ...this.getContext(requestId), ...metadata });
}
};
}
/**
* Create a child logger for a specific component
* @param component Component name
* @returns Logger with component in metadata
*/
static getComponentLogger(component) {
return {
debug: (message, metadata) => {
logger.debug(message, { component, ...metadata });
},
info: (message, metadata) => {
logger.info(message, { component, ...metadata });
},
warn: (message, metadata) => {
logger.warn(message, { component, ...metadata });
},
error: (message, metadata) => {
logger.error(message, { component, ...metadata });
}
};
}
}
LogContext.requestContext = new Map();
/**
* Format function to handle Error objects in metadata
* Extracts relevant properties from Error objects for better serialization
*/
const handleErrorObjects = format((info) => {
// Handle any Error objects in the metadata
const processedInfo = { ...info };
Object.keys(processedInfo).forEach(key => {
if (processedInfo[key] instanceof Error) {
const error = processedInfo[key];
processedInfo[key] = {
message: error.message,
stack: error.stack,
name: error.name,
// Extract any custom properties from the error
...(Object.getOwnPropertyNames(error)
.filter(prop => !['message', 'stack', 'name'].includes(prop))
.reduce((obj, prop) => {
obj[prop] = error[prop];
return obj;
}, {}))
};
}
});
return processedInfo;
});
/**
* Format for console output - more human-readable
*/
const consoleFormat = format.combine(format.colorize(), format.timestamp(), format.printf(({ level, message, timestamp, ...metadata }) => {
// Extract important metadata for console display
const { requestId, component, operation, duration, statusCode } = metadata;
// Format important metadata into a concise string
let metadataStr = '';
if (requestId)
metadataStr += `requestId=${requestId} `;
if (component)
metadataStr += `component=${component} `;
if (operation)
metadataStr += `operation=${operation} `;
if (duration)
metadataStr += `duration=${duration}ms `;
if (statusCode)
metadataStr += `status=${statusCode} `;
return `${timestamp} ${level}: ${message} ${metadataStr}`;
}));
/**
* Configure and create the Winston logger instance
*
* The logger uses the following configuration:
* - Log level: Configured via LOG_LEVEL environment variable (defaults to 'info')
* - Format: JSON with timestamps for file output, colorized simple format for console
* - Transports:
* - Console: All levels, with colorization
* - Error File: Only error level messages in 'error.log'
* - Combined File: All levels in 'combined.log'
*/
const logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
format: format.combine(format.timestamp(), addStandardMetadata(), handleErrorObjects(), format.json()),
transports: [
new transports.Console({
format: consoleFormat
}),
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' })
],
exitOnError: false // Don't crash on logger error
});
/**
* Helper function to create a scoped logger with standard metadata
* @param component Component name for categorizing logs
* @returns A logger instance with component metadata
*/
export function createComponentLogger(component) {
return LogContext.getComponentLogger(component);
}
/**
* Helper function to log performance metrics
* @param operation Operation name
* @param startTime Start time in milliseconds
* @param metadata Additional metadata
*/
export function logPerformance(operation, startTime, metadata = {}) {
const duration = Date.now() - startTime;
logger.debug(`Performance: ${operation}`, {
operation,
duration,
...metadata
});
}
/**
* The configured logger instance for use throughout the application
*
* @example
* import logger from './logger.js';
*
* // Basic logging
* logger.debug('Detailed debugging information');
* logger.info('General operational information');
* logger.warn('Warning conditions');
* logger.error('Error conditions', { error: err });
*
* // Structured logging with metadata
* logger.info('User logged in', { userId: 123, role: 'admin' });
*
* // Component-specific logging
* import { createComponentLogger } from './logger.js';
* const dbLogger = createComponentLogger('database');
* dbLogger.info('Connected to database', { database: 'users' });
*
* // Request-scoped logging
* import { LogContext } from './logger.js';
* const requestId = LogContext.initRequest(sessionId);
* const reqLogger = LogContext.getContextLogger(requestId);
* reqLogger.info('Processing request', { endpoint: '/api/users' });
* // Later when done
* LogContext.endRequest(requestId);
*
* // Performance logging
* import { logPerformance } from './logger.js';
* const startTime = Date.now();
* // ... do operation ...
* logPerformance('fetch-users', startTime, { count: 10 });
*/
export default logger;
//# sourceMappingURL=logger.js.map