/**
* @fileoverview Simple logging service for the application.
* This provides a centralized place for all logging functionality.
*
* Configuration:
* - LOG_LEVEL: Sets the minimum log level (DEBUG, INFO, WARN, ERROR). Defaults to DEBUG.
* - LOG_FILE: Path to the log file. If not set, no logs will be written.
*
* Note: Since MCP servers use stdout for protocol communication, logs are written
* to a file instead of stdout/stderr to avoid interference.
*/
import { writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';
/**
* Log levels for the application
* @enum {string}
*/
// This enum is part of the public API and is used by consumers, even if not all values are used in this file
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}
/**
* Environment-aware logging configuration
*/
const LOG_LEVELS_PRIORITY: Record<LogLevel, number> = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
[LogLevel.WARN]: 2,
[LogLevel.ERROR]: 3,
};
/**
* Get the log file path from environment
* @returns {string | null} The log file path or null if not configured
* @private
*/
function getLogFilePath(): string | null {
return process.env.LOG_FILE || null;
}
let logFileInitialized = false;
/**
* Initialize the log file if needed by creating the directory and file
* Only initializes once per process to avoid redundant file operations
* @private
* @returns {void}
*/
function initializeLogFile(): void {
const logFile = getLogFilePath();
if (logFile && !logFileInitialized) {
try {
// Create directory if it doesn't exist
const dir = dirname(logFile);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
// Create or truncate the log file
writeFileSync(logFile, '');
logFileInitialized = true;
} catch {
// Fail silently if we can't create the log file
logFileInitialized = true; // Don't retry
}
}
}
/**
* Write a log message to file
* @param message The formatted log message to write
* @private
*/
function writeToLogFile(message: string): void {
const logFile = getLogFilePath();
if (logFile) {
try {
if (!logFileInitialized) {
initializeLogFile();
}
appendFileSync(logFile, `${message}\n`);
} catch {
// Fail silently if we can't write to the log file
}
}
}
/**
* Check if a log level should be displayed based on the environment configuration
* @param level The log level to check
* @returns {boolean} True if the log level should be displayed
* @private
*/
function shouldLog(level: LogLevel): boolean {
const configuredLevel = (process.env.LOG_LEVEL || 'DEBUG') as LogLevel;
return LOG_LEVELS_PRIORITY[level] >= LOG_LEVELS_PRIORITY[configuredLevel];
}
/**
* Format a log message with timestamp, level, and context information
* @param level The log level of the message
* @param message The log message content
* @param context Optional context identifier
* @returns {string} Formatted log message
* @private
*/
function formatLogMessage(level: LogLevel, message: string, context?: string): string {
const timestamp = new Date().toISOString();
const contextStr = context ? `[${context}] ` : '';
return `${timestamp} ${level} ${contextStr}${message}`;
}
/**
* Logger service for consistent logging throughout the application
*/
export class Logger {
private context?: string;
/**
* Create a new logger instance, optionally with a context
* @param context Optional context name to identify the log source
*/
constructor(context?: string) {
if (context !== undefined) {
this.context = context;
}
}
/**
* Log a debug message
* @param message The message to log
* @param data Optional data to include in the log
*/
debug(message: string, data?: unknown): void {
if (shouldLog(LogLevel.DEBUG) && getLogFilePath()) {
const formattedMessage = formatLogMessage(LogLevel.DEBUG, message, this.context);
const fullMessage =
data !== undefined
? `${formattedMessage} ${JSON.stringify(data, null, 2)}`
: formattedMessage;
writeToLogFile(fullMessage);
}
}
/**
* Log an info message
* @param message The message to log
* @param data Optional data to include in the log
*/
info(message: string, data?: unknown): void {
if (shouldLog(LogLevel.INFO) && getLogFilePath()) {
const formattedMessage = formatLogMessage(LogLevel.INFO, message, this.context);
const fullMessage =
data !== undefined
? `${formattedMessage} ${JSON.stringify(data, null, 2)}`
: formattedMessage;
writeToLogFile(fullMessage);
}
}
/**
* Log a warning message
* @param message The message to log
* @param data Optional data to include in the log
*/
warn(message: string, data?: unknown): void {
if (shouldLog(LogLevel.WARN) && getLogFilePath()) {
const formattedMessage = formatLogMessage(LogLevel.WARN, message, this.context);
const fullMessage =
data !== undefined
? `${formattedMessage} ${JSON.stringify(data, null, 2)}`
: formattedMessage;
writeToLogFile(fullMessage);
}
}
/**
* Log an error message with improved error formatting
* @param message The message to log
* @param error Optional error to include in the log. The error will be formatted for better readability:
* - Error objects will include name, message and stack trace
* - Objects will be stringified with proper indentation
* - Other values will be converted to strings
*/
error(message: string, error?: unknown): void {
if (shouldLog(LogLevel.ERROR) && getLogFilePath()) {
const formattedMessage = formatLogMessage(LogLevel.ERROR, message, this.context);
// Format the error for better debugging
let errorOutput = '';
if (error !== undefined) {
if (error instanceof Error) {
errorOutput = `${error.name}: ${error.message}${error.stack ? `\n${error.stack}` : ''}`;
} else {
try {
errorOutput = JSON.stringify(error, null, 2);
} catch {
// Fallback to string representation if JSON.stringify fails
errorOutput = String(error);
}
}
}
const fullMessage = errorOutput ? `${formattedMessage} ${errorOutput}` : formattedMessage;
writeToLogFile(fullMessage);
}
}
}
/**
* Default logger instance for the application
* Pre-configured with the 'DeepSourceMCP' context for quick imports
* @const {Logger}
*/
export const defaultLogger = new Logger('DeepSourceMCP');
/**
* Helper function to create a logger with a specific context
* @param context The context to use for the logger
* @returns A new logger instance with the specified context
*/
export function createLogger(context: string): Logger {
return new Logger(context);
}
/**
* Default export for simpler imports
*/
export default defaultLogger;
// These log levels define the hierarchy for logging operations:
// - DEBUG: Detailed information for debugging
// - INFO: General information about application operation
// - WARN: Warning events that might lead to issues
// - ERROR: Error events that might still allow the application to continue
// This helps maintain consistent logging patterns throughout the application