Skip to main content
Glama
index.ts11.9 kB
/** * Centralized Logger Utility for TeamCity MCP Server * * This module provides a unified logging interface that consolidates * the existing logger implementations and enforces consistent logging * patterns throughout the application. */ import { existsSync, mkdirSync } from 'node:fs'; import winston, { type Logger } from 'winston'; /** * Log levels supported by the logger */ export type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly'; /** * Context information for logging */ export interface LogContext { /** * Tool name (for MCP tool execution) */ toolName?: string; /** * Unique request identifier */ requestId?: string; /** * Session identifier */ sessionId?: string; /** * User identifier */ userId?: string; /** * Operation duration in milliseconds */ duration?: number; /** * Service or module name */ service?: string; /** * TeamCity build ID */ buildId?: string; /** * TeamCity project ID */ projectId?: string; /** * Additional context properties */ [key: string]: unknown; } /** * Logger configuration options */ export interface LoggerConfig { name?: string; level?: LogLevel; enableConsole?: boolean; enableFile?: boolean; logDirectory?: string; maxFileSize?: string; maxFiles?: number; } /** * Logger interface for type safety and consistency */ export interface ILogger { debug(message: string, context?: LogContext): void; info(message: string, context?: LogContext): void; warn(message: string, context?: LogContext): void; error(message: string, error?: Error | unknown, context?: LogContext): void; child(context: LogContext): ILogger; } /** * Enhanced logger implementation */ export class TeamCityLogger implements ILogger { private winston: Logger; private config: Required<LoggerConfig>; private requestCounter = 0; constructor(config: LoggerConfig = {}) { this.config = this.normalizeConfig(config); this.winston = this.createWinstonLogger(); } /** * Normalize configuration with defaults */ private normalizeConfig(config: LoggerConfig): Required<LoggerConfig> { const isProduction = process.env['NODE_ENV'] === 'production'; const isDevelopment = process.env['NODE_ENV'] === 'development'; const forceFile = process.env['TEAMCITY_LOG_TO_FILE'] === '1' || process.env['TEAMCITY_LOG_TO_FILE'] === 'true'; return { name: config.name ?? 'teamcity-mcp', level: config.level ?? (process.env['LOG_LEVEL'] as LogLevel) ?? (isDevelopment ? 'debug' : 'info'), enableConsole: config.enableConsole ?? true, // Allow forcing file logging via env during tests or local runs enableFile: config.enableFile ?? (isProduction || forceFile), logDirectory: config.logDirectory ?? 'logs', maxFileSize: config.maxFileSize ?? '10m', maxFiles: config.maxFiles ?? 5, }; } /** * Create Winston logger instance */ private createWinstonLogger(): Logger { const { level, enableConsole, enableFile, logDirectory, maxFileSize, maxFiles, name } = this.config; const isProduction = process.env['NODE_ENV'] === 'production'; // Custom format for development const devFormat = winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'HH:mm:ss.SSS' }), winston.format.errors({ stack: true }), winston.format.printf(({ timestamp, level, message, service, ...meta }) => { const baseLog = `${timestamp} [${service}] ${level}: ${message}`; return this.formatContextualLog(baseLog, meta); }) ); // JSON format for production const prodFormat = winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ); const transports: winston.transport[] = []; // Console transport // For MCP stdio transport compliance, ALL console output must go to stderr if (enableConsole) { transports.push( new winston.transports.Console({ format: isProduction ? prodFormat : devFormat, stderrLevels: ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'], }) ); } // File transports if (enableFile) { this.ensureLogDirectory(logDirectory); // Error-only log file transports.push( new winston.transports.File({ filename: `${logDirectory}/error.log`, level: 'error', format: prodFormat, maxsize: this.parseFileSize(maxFileSize), maxFiles, }) ); // Combined log file transports.push( new winston.transports.File({ filename: `${logDirectory}/combined.log`, format: prodFormat, maxsize: this.parseFileSize(maxFileSize), maxFiles, }) ); } return winston.createLogger({ level, defaultMeta: { service: name }, transports, exitOnError: false, }); } /** * Format contextual log with MCP-specific information */ private formatContextualLog(baseLog: string, context: LogContext): string { const { toolName, requestId, sessionId, duration, buildId, projectId, ...otherMeta } = context; const contextParts: string[] = []; if (requestId) contextParts.push(`req=${requestId}`); if (sessionId) contextParts.push(`session=${sessionId}`); if (toolName) contextParts.push(`tool=${toolName}`); if (buildId) contextParts.push(`build=${buildId}`); if (projectId) contextParts.push(`project=${projectId}`); if (duration !== undefined) contextParts.push(`${duration}ms`); const contextString = contextParts.length > 0 ? ` [${contextParts.join(' ')}]` : ''; const metaString = Object.keys(otherMeta).length > 0 ? ` ${JSON.stringify(otherMeta)}` : ''; return `${baseLog}${contextString}${metaString}`; } /** * Ensure log directory exists */ private ensureLogDirectory(directory: string): void { try { if (existsSync(directory) === false) { mkdirSync(directory, { recursive: true }); } } catch (error) { // Fallback to current directory if log directory creation fails this.winston?.warn?.('Failed to create log directory, using current directory', { error }); } } /** * Parse file size string to bytes */ private parseFileSize(size: string): number { const match = size.match(/^(\d+)([kmg]?)$/i); if (!match) return 10 * 1024 * 1024; // Default 10MB const [, num, unit] = match; if (!num) return 10 * 1024 * 1024; // Default 10MB const multiplier = { '': 1, k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024, }[unit?.toLowerCase() ?? ''] ?? 1; return parseInt(num, 10) * multiplier; } /** * Generate unique request ID */ public generateRequestId(): string { this.requestCounter++; return `${Date.now()}-${this.requestCounter}`; } /** * Debug level logging */ public debug(message: string, context: LogContext = {}): void { this.winston.debug(message, context); } /** * Info level logging */ public info(message: string, context: LogContext = {}): void { this.winston.info(message, context); } /** * Warning level logging */ public warn(message: string, context: LogContext = {}): void { this.winston.warn(message, context); } /** * Error level logging with optional error object */ public error(message: string, error?: Error | unknown, context: LogContext = {}): void { const errorContext = { ...context }; if (error instanceof Error) { errorContext['error'] = error.message; errorContext['stack'] = error.stack; } else if (error != null) { errorContext['error'] = String(error); } this.winston.error(message, errorContext); } /** * Create child logger with additional context */ public child(context: LogContext): ILogger { const childLogger = new TeamCityLogger(this.config); childLogger.winston = this.winston.child(context); return childLogger; } /** * Log tool execution with performance metrics */ public logToolExecution( toolName: string, args: Record<string, unknown>, result: { success: boolean; error?: string }, duration: number, context: LogContext = {} ): void { const toolContext: LogContext = { ...context, toolName, duration, args: JSON.stringify(args), success: result.success, }; if (result.success) { this.info(`Tool executed successfully: ${toolName}`, toolContext); } else { this.error(`Tool execution failed: ${toolName}`, result.error, toolContext); } } /** * Log TeamCity API interactions */ public logTeamCityRequest( method: string, url: string, status?: number, duration?: number, context: LogContext = {} ): void { const requestContext: LogContext = { ...context, method, url, status, duration, }; if (status && status >= 400) { this.warn(`TeamCity API request failed: ${method} ${url}`, requestContext); } else { this.debug(`TeamCity API request: ${method} ${url}`, requestContext); } } /** * Log server lifecycle events */ public logLifecycle(event: string, details?: Record<string, unknown>): void { this.info(`Server lifecycle: ${event}`, { lifecycle: event, ...details }); } /** * Set log level dynamically */ public setLevel(level: LogLevel): void { this.winston.level = level; this.config.level = level; } /** * Get current log level */ public getLevel(): LogLevel { return this.winston.level as LogLevel; } /** * Get underlying Winston instance (for advanced usage) */ public getWinstonInstance(): Logger { return this.winston; } } // Default logger instance let defaultLogger: TeamCityLogger | null = null; /** * Get or create the default logger instance */ export function getLogger(config?: LoggerConfig): TeamCityLogger { if (!defaultLogger || config) { defaultLogger = new TeamCityLogger(config); } return defaultLogger; } /** * Create a new logger instance */ export function createLogger(config?: LoggerConfig): TeamCityLogger { return new TeamCityLogger(config); } /** * Convenience functions using the default logger */ export const logger = { debug: (message: string, context?: LogContext) => getLogger().debug(message, context), info: (message: string, context?: LogContext) => getLogger().info(message, context), warn: (message: string, context?: LogContext) => getLogger().warn(message, context), error: (message: string, error?: Error | unknown, context?: LogContext) => getLogger().error(message, error, context), child: (context: LogContext) => getLogger().child(context), logToolExecution: ( toolName: string, args: Record<string, unknown>, result: { success: boolean; error?: string }, duration: number, context?: LogContext ) => getLogger().logToolExecution(toolName, args, result, duration, context), logTeamCityRequest: ( method: string, url: string, status?: number, duration?: number, context?: LogContext ) => getLogger().logTeamCityRequest(method, url, status, duration, context), logLifecycle: (event: string, details?: Record<string, unknown>) => getLogger().logLifecycle(event, details), }; // Export types for external usage // Backward compatibility exports export const createTeamCityLogger = createLogger; export const getTeamCityLogger = getLogger; // Export individual convenience functions for direct import export const { debug, info, warn, error, child } = logger;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server