UNPKG

obsidian-mcp-server

Version:

Model Context Protocol (MCP) server designed for LLMs to interact with Obsidian vaults. Provides secure, token-aware tools for seamless knowledge base management through a standardized interface.

417 lines 14.4 kB
/** * Standardized Logging System for the Obsidian MCP Server * * This module provides structured logging capabilities aligned with MCP best practices. * It supports categorized errors, severity levels, context tracking, performance * monitoring, and secure logging patterns. * * @module utils/logging */ import fs from "fs"; import path from "path"; import process from "process"; import winston from "winston"; /** * Error categories following MCP standards */ export var ErrorCategoryType; (function (ErrorCategoryType) { ErrorCategoryType["CATEGORY_VALIDATION"] = "VALIDATION"; ErrorCategoryType["CATEGORY_AUTHENTICATION"] = "AUTHENTICATION"; ErrorCategoryType["CATEGORY_AUTHORIZATION"] = "AUTHORIZATION"; ErrorCategoryType["CATEGORY_BUSINESS_LOGIC"] = "BUSINESS_LOGIC"; ErrorCategoryType["CATEGORY_DATA_ACCESS"] = "DATA_ACCESS"; ErrorCategoryType["CATEGORY_EXTERNAL_SERVICE"] = "EXTERNAL_SERVICE"; ErrorCategoryType["CATEGORY_SYSTEM"] = "SYSTEM"; ErrorCategoryType["CATEGORY_UNKNOWN"] = "UNKNOWN"; })(ErrorCategoryType || (ErrorCategoryType = {})); /** * Log levels aligned with MCP's ErrorSeverityLevel */ export var LogLevel; (function (LogLevel) { /** Critical errors that require immediate attention */ LogLevel[LogLevel["ERROR"] = 0] = "ERROR"; /** Potentially harmful situations that should be reviewed */ LogLevel[LogLevel["WARN"] = 1] = "WARN"; /** General informational messages about system operation */ LogLevel[LogLevel["INFO"] = 2] = "INFO"; /** Detailed information for debugging purposes */ LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG"; /** Highly detailed tracing information */ LogLevel[LogLevel["TRACE"] = 4] = "TRACE"; })(LogLevel || (LogLevel = {})); /** * Maps LogLevel to MCP severity level for integration with MCP error handling */ export const logLevelToMcpSeverity = { [LogLevel.ERROR]: 3, // SEVERITY_ERROR [LogLevel.WARN]: 2, // SEVERITY_WARN [LogLevel.INFO]: 1, // SEVERITY_INFO [LogLevel.DEBUG]: 0, // SEVERITY_DEBUG [LogLevel.TRACE]: 0 // Also maps to SEVERITY_DEBUG }; /** * Winston-compatible log levels */ const winstonLogLevels = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 }; /** * Default logger configuration */ const DEFAULT_CONFIG = { level: process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG, includeTimestamps: true, includeLevel: true, maskSensitiveData: true, sensitiveFields: ['password', 'token', 'secret', 'key', 'auth', 'credential'], logDir: "logs", files: true, fileNames: { combined: "combined.log", error: "error.log", warn: "warn.log", info: "info.log", debug: "debug.log" } }; /** * Enhanced logger for MCP server operations with structured logging support * Implements file-based logging with zero console output */ export class Logger { name; config; timers = new Map(); logger; /** * Creates a new logger instance * * @param name - Component name for this logger * @param config - Optional configuration overrides */ constructor(name, config = {}) { this.name = name; this.config = { ...DEFAULT_CONFIG, ...config, fileNames: { ...DEFAULT_CONFIG.fileNames, ...config.fileNames } }; // Initialize logger with silent transport as fallback this.logger = winston.createLogger({ levels: winstonLogLevels, defaultMeta: { component: this.name }, transports: [ // Silent transport to prevent "no transports" warning new winston.transports.Console({ silent: true }) ] }); this.initializeLogger(); } /** * Initialize or reinitialize the Winston logger */ initializeLogger() { if (this.config.files && this.config.logDir) { if (!fs.existsSync(this.config.logDir)) { try { fs.mkdirSync(this.config.logDir, { recursive: true }); } catch (error) { // Silently continue - we have a silent transport as fallback } } } // Create log format const logFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()); // Initialize transports array with a silent transport to avoid "no transports" warning const transports = [ new winston.transports.Console({ silent: true, // Silent transport that doesn't output logs format: logFormat }) ]; // Add file transports if enabled if (this.config.files && this.config.logDir) { try { const fileNames = this.config.fileNames || DEFAULT_CONFIG.fileNames; // Combined log file if (fileNames?.combined) { transports.push(new winston.transports.File({ filename: path.join(this.config.logDir, fileNames.combined), format: logFormat })); } // Level-specific log files if (fileNames?.error) { transports.push(new winston.transports.File({ filename: path.join(this.config.logDir, fileNames.error), level: 'error', format: logFormat })); } if (fileNames?.warn) { transports.push(new winston.transports.File({ filename: path.join(this.config.logDir, fileNames.warn), level: 'warn', format: logFormat })); } if (fileNames?.info) { transports.push(new winston.transports.File({ filename: path.join(this.config.logDir, fileNames.info), level: 'info', format: logFormat })); } if (fileNames?.debug) { transports.push(new winston.transports.File({ filename: path.join(this.config.logDir, fileNames.debug), level: 'debug', format: logFormat })); } } catch (error) { // Silently continue with just the silent transport if file transports fail } } // Replace logger configuration this.logger.configure({ levels: winstonLogLevels, level: LogLevel[this.config.level].toLowerCase(), defaultMeta: { component: this.name }, transports }); } /** * Maps internal LogLevel to Winston log level */ levelToWinstonLevel(level) { const levelName = LogLevel[level].toLowerCase(); return levelName === 'trace' ? 'debug' : levelName; } /** * Creates a standardized error object for MCP compatibility * * @param message - Human-readable error description * @param code - Machine-readable error identifier * @param category - Type of error * @param context - Additional error context * @returns Standardized error object */ createStandardizedError(message, code, category = ErrorCategoryType.CATEGORY_UNKNOWN, context) { return { errorMessage: message, errorCode: code, errorCategory: category, errorSeverity: LogLevel.ERROR, errorTimestamp: new Date().toISOString(), errorContext: context ? this.processSensitiveData(context) : undefined, errorStack: new Error().stack }; } /** * Wraps an exception as a standardized error * * @param error - Original error object * @param message - Optional override message * @param category - Error category * @returns Standardized error object */ wrapExceptionAsStandardizedError(error, message, category = ErrorCategoryType.CATEGORY_UNKNOWN) { const errorObject = error instanceof Error ? error : new Error(String(error)); return { errorMessage: message || errorObject.message, errorCode: errorObject.name || 'UNKNOWN_ERROR', errorCategory: category, errorSeverity: LogLevel.ERROR, errorTimestamp: new Date().toISOString(), errorStack: errorObject.stack }; } /** * Masks sensitive data in objects before logging * * @param data - Data object to process * @returns Processed data with sensitive fields masked */ processSensitiveData(data) { if (!this.config.maskSensitiveData) return data; const result = {}; const sensitiveFields = this.config.sensitiveFields; for (const [key, value] of Object.entries(data)) { // Check if key contains any sensitive field name const isSensitive = sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase())); if (isSensitive) { // Mask sensitive values result[key] = '********'; } else if (value && typeof value === 'object' && !Array.isArray(value)) { // Recursively process nested objects result[key] = this.processSensitiveData(value); } else { // Pass through non-sensitive values result[key] = value; } } return result; } /** * Internal method to log a message using Winston * * @param level - Log level * @param message - Log message * @param context - Optional context data */ log(level, message, context) { if (level > this.config.level) return; const winstonLevel = this.levelToWinstonLevel(level); const processedContext = context ? this.processSensitiveData(context) : undefined; this.logger.log(winstonLevel, message, { context: processedContext }); } /** * Log an error message with optional error object and context * * @param message - Error message * @param errorOrContext - Error object or context * @param context - Additional context */ error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { const errorObj = this.wrapExceptionAsStandardizedError(errorOrContext); const combinedContext = { ...(context || {}), error: errorObj }; this.log(LogLevel.ERROR, message, combinedContext); } else { this.log(LogLevel.ERROR, message, errorOrContext || context); } } /** * Log a warning message * * @param message - Warning message * @param context - Optional context data */ warn(message, context) { this.log(LogLevel.WARN, message, context); } /** * Log an info message * * @param message - Info message * @param context - Optional context data */ info(message, context) { this.log(LogLevel.INFO, message, context); } /** * Log a debug message * * @param message - Debug message * @param context - Optional context data */ debug(message, context) { this.log(LogLevel.DEBUG, message, context); } /** * Log a trace message * * @param message - Trace message * @param context - Optional context data */ trace(message, context) { this.log(LogLevel.TRACE, message, context); } /** * Set the log level * * @param level - New log level */ setLevel(level) { this.config.level = level; this.logger.level = this.levelToWinstonLevel(level); } /** * Start a timer for performance tracking * * @param id - Timer identifier */ startTimer(id) { this.timers.set(id, performance.now()); } /** * End a timer and log the elapsed time * * @param id - Timer identifier * @param message - Log message prefix * @param level - Log level for the timing message */ endTimer(id, message = 'Operation completed', level = LogLevel.DEBUG) { const startTime = this.timers.get(id); if (startTime === undefined) { this.warn(`Timer "${id}" does not exist`, { action: 'endTimer' }); return 0; } const endTime = performance.now(); const elapsedMs = endTime - startTime; this.log(level, `${message} in ${elapsedMs.toFixed(2)}ms`, { timerId: id, processingTimeMs: elapsedMs }); this.timers.delete(id); return elapsedMs; } /** * Log the result of an operation with timing information * * @param success - Whether the operation was successful * @param operation - Operation name * @param elapsedMs - Processing time in milliseconds * @param context - Optional operation context */ logOperationResult(success, operation, elapsedMs, context) { const level = success ? LogLevel.INFO : LogLevel.ERROR; const status = success ? 'succeeded' : 'failed'; this.log(level, `Operation ${operation} ${status}`, { ...(context || {}), operation, success, processingTimeMs: elapsedMs }); } } /** * Create and return a namespaced logger * * @param name - Component name for the logger * @param config - Optional configuration overrides * @returns Configured logger instance */ export function createLogger(name, config) { return new Logger(name, config); } // Create a global root logger export const rootLogger = createLogger('obsidian-mcp-server', { files: true, logDir: "logs" }); //# sourceMappingURL=logging.js.map