import winston from 'winston';
import { mkdirSync, existsSync } from 'fs';
import { join } from 'path';
export interface PerformanceMetrics {
requestCount: number;
errorCount: number;
averageResponseTime: number;
awsApiCallCount: number;
authEventCount: number;
lastResetTime: Date;
}
export interface MonitoringData {
timestamp: Date;
level: string;
message: string;
service: string;
type?: string;
duration?: number;
success?: boolean;
userId?: string;
toolName?: string;
errorCode?: string;
[key: string]: any;
}
export class Logger {
private static instance: Logger;
private logger: winston.Logger;
private metrics: PerformanceMetrics;
private responseTimes: number[];
private maxResponseTimeHistory: number;
private constructor() {
this.metrics = {
requestCount: 0,
errorCount: 0,
averageResponseTime: 0,
awsApiCallCount: 0,
authEventCount: 0,
lastResetTime: new Date()
};
this.responseTimes = [];
this.maxResponseTimeHistory = 1000; // Keep last 1000 response times for average calculation
// Ensure logs directory exists
this.ensureLogsDirectory();
this.logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf((info) => {
// Add correlation ID if available
const correlationId = (global as any).correlationId || 'unknown';
return JSON.stringify({
...info,
correlationId,
pid: process.pid,
memory: process.memoryUsage(),
uptime: process.uptime()
});
})
),
defaultMeta: { service: 'aws-billing-mcp-server' },
transports: [
new winston.transports.File({
filename: this.getLogPath('error.log'),
level: 'error',
maxsize: 10 * 1024 * 1024, // 10MB
maxFiles: 5
}),
new winston.transports.File({
filename: this.getLogPath('combined.log'),
maxsize: 50 * 1024 * 1024, // 50MB
maxFiles: 10
}),
new winston.transports.File({
filename: this.getLogPath('performance.log'),
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
winston.format.printf((info) => {
// Only log performance-related entries
if (info.type && ['request', 'aws_api', 'performance'].includes(info.type as string)) {
return JSON.stringify(info);
}
return '';
})
)
}),
new winston.transports.File({
filename: this.getLogPath('security.log'),
level: 'warn',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
winston.format.printf((info) => {
// Only log security-related entries
if (info.type === 'security' || info.type === 'auth') {
return JSON.stringify(info);
}
return '';
})
)
})
],
});
// Add console transport for development, or stderr-only for MCP server mode
if (process.env.NODE_ENV !== 'production') {
if (process.env.MCP_SERVER_MODE) {
// In MCP mode, only log errors and warnings to stderr
this.logger.add(new winston.transports.Console({
level: 'warn',
stderrLevels: ['error', 'warn'],
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf((info) => {
return `${info.timestamp} [${info.level}]: ${info.message}`;
})
)
}));
} else {
// Normal development mode with full console output
this.logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf((info) => {
const correlationId = (global as any).correlationId || 'unknown';
return `${info.timestamp} [${correlationId}] ${info.level}: ${info.message} ${
Object.keys(info).length > 3 ? JSON.stringify(info, null, 2) : ''
}`;
})
)
}));
}
}
// Log metrics every 5 minutes
setInterval(() => {
this.logPerformanceMetrics();
}, 5 * 60 * 1000);
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public info(message: string, meta?: Record<string, unknown>): void {
this.logger.info(message, meta);
}
public error(message: string, meta?: Record<string, unknown>): void {
this.logger.error(message, meta);
}
public warn(message: string, meta?: Record<string, unknown>): void {
this.logger.warn(message, meta);
}
public debug(message: string, meta?: Record<string, unknown>): void {
this.logger.debug(message, meta);
}
public logRequest(method: string, path: string, responseTime: number, statusCode: number, userId?: string): void {
this.metrics.requestCount++;
this.updateResponseTime(responseTime);
if (statusCode >= 400) {
this.metrics.errorCount++;
}
this.logger.info('Request processed', {
method,
path,
responseTime,
statusCode,
userId,
type: 'request'
});
}
public logToolCall(toolName: string, duration: number, success: boolean, userId?: string, errorCode?: string): void {
this.metrics.requestCount++;
this.updateResponseTime(duration);
if (!success) {
this.metrics.errorCount++;
}
this.logger.info('Tool call processed', {
toolName,
duration,
success,
userId,
errorCode,
type: 'tool_call'
});
}
public logAWSAPICall(service: string, operation: string, duration: number, success: boolean): void {
this.metrics.awsApiCallCount++;
this.logger.info('AWS API call', {
awsService: service,
operation,
duration,
success,
type: 'aws_api'
});
}
public logAuthEvent(event: string, userId?: string, success?: boolean, details?: Record<string, unknown>): void {
this.metrics.authEventCount++;
const logData = {
event,
userId,
success,
...details,
type: 'auth'
};
if (success === false) {
this.logger.warn('Authentication event', logData);
} else {
this.logger.info('Authentication event', logData);
}
}
public logSecurityEvent(event: string, details: Record<string, unknown>): void {
this.logger.warn('Security event', {
event,
...details,
type: 'security',
severity: 'high'
});
}
public logPerformanceMetrics(): void {
const currentMetrics = { ...this.metrics };
this.logger.info('Performance metrics', {
...currentMetrics,
type: 'performance',
memoryUsage: process.memoryUsage(),
cpuUsage: process.cpuUsage(),
uptime: process.uptime()
});
}
public logDataAccess(operation: string, recordCount: number, duration: number, userId?: string): void {
this.logger.info('Data access', {
operation,
recordCount,
duration,
userId,
type: 'data_access'
});
}
public logCacheOperation(operation: 'hit' | 'miss' | 'set' | 'invalidate', key: string, duration?: number): void {
this.logger.debug('Cache operation', {
operation,
key,
duration,
type: 'cache'
});
}
public logBusinessEvent(event: string, details: Record<string, unknown>): void {
this.logger.info('Business event', {
event,
...details,
type: 'business'
});
}
public getMetrics(): PerformanceMetrics {
return { ...this.metrics };
}
public resetMetrics(): void {
this.metrics = {
requestCount: 0,
errorCount: 0,
averageResponseTime: 0,
awsApiCallCount: 0,
authEventCount: 0,
lastResetTime: new Date()
};
this.responseTimes = [];
}
public getHealthStatus(): { status: 'healthy' | 'degraded' | 'unhealthy', details: Record<string, any> } {
const errorRate = this.metrics.requestCount > 0 ? (this.metrics.errorCount / this.metrics.requestCount) * 100 : 0;
const avgResponseTime = this.metrics.averageResponseTime;
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
if (errorRate > 10 || avgResponseTime > 5000) {
status = 'unhealthy';
} else if (errorRate > 5 || avgResponseTime > 2000) {
status = 'degraded';
}
return {
status,
details: {
errorRate: `${errorRate.toFixed(2)}%`,
averageResponseTime: `${avgResponseTime.toFixed(2)}ms`,
totalRequests: this.metrics.requestCount,
totalErrors: this.metrics.errorCount,
uptime: `${Math.floor(process.uptime())}s`,
memoryUsage: process.memoryUsage(),
lastMetricsReset: this.metrics.lastResetTime
}
};
}
public setCorrelationId(id: string): void {
(global as any).correlationId = id;
}
public getCorrelationId(): string {
return (global as any).correlationId || 'unknown';
}
private updateResponseTime(responseTime: number): void {
this.responseTimes.push(responseTime);
// Keep only the last N response times
if (this.responseTimes.length > this.maxResponseTimeHistory) {
this.responseTimes = this.responseTimes.slice(-this.maxResponseTimeHistory);
}
// Calculate average
this.metrics.averageResponseTime = this.responseTimes.reduce((sum, time) => sum + time, 0) / this.responseTimes.length;
}
private ensureLogsDirectory(): void {
const logsDir = this.getLogsDirectory();
if (!existsSync(logsDir)) {
try {
mkdirSync(logsDir, { recursive: true });
} catch (error) {
// If we can't create logs directory, we'll fall back to stderr only
process.stderr.write(`Could not create logs directory: ${error}\n`);
}
}
}
private getLogsDirectory(): string {
// Try to use the project's logs directory, fall back to temp directory
const projectLogsDir = join(process.cwd(), 'logs');
// Check if we're in the project directory (has package.json)
if (existsSync(join(process.cwd(), 'package.json'))) {
return projectLogsDir;
}
// Fall back to a temp directory in the user's home
const homeLogsDir = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.aws-billing-mcp-server', 'logs');
return homeLogsDir;
}
private getLogPath(filename: string): string {
return join(this.getLogsDirectory(), filename);
}
}