package logger import ( "fmt" "log" "os" "path/filepath" "strings" "time" ) // LogLevel represents different log levels type LogLevel int const ( DEBUG LogLevel = iota INFO WARN ERROR OPPORTUNITY // Special level for opportunities ) var logLevelNames = map[LogLevel]string{ DEBUG: "DEBUG", INFO: "INFO", WARN: "WARN", ERROR: "ERROR", OPPORTUNITY: "OPPORTUNITY", } var suppressedWarningSubstrings = []string{ "extractTokensGeneric", "extractTokensFromMulticall", } // Logger represents a multi-file logger with separation of concerns type Logger struct { // Main application logger logger *log.Logger level LogLevel // Specialized loggers for different concerns opportunityLogger *log.Logger // MEV opportunities and arbitrage attempts errorLogger *log.Logger // Errors and warnings only performanceLogger *log.Logger // Performance metrics and RPC calls transactionLogger *log.Logger // Detailed transaction analysis // Security filtering secureFilter *SecureFilter levelName string } // parseLogLevel converts string log level to LogLevel enum func parseLogLevel(level string) LogLevel { switch strings.ToLower(level) { case "debug": return DEBUG case "info": return INFO case "warn", "warning": return WARN case "error": return ERROR default: return INFO // Default to INFO level } } // createLogFile creates a log file or returns stdout if it fails func createLogFile(filename string) *os.File { if filename == "" { return os.Stdout } if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil { log.Printf("Failed to create log directory for %s: %v, falling back to stdout", filename, err) return os.Stdout } // Check and rotate log file if needed (100MB max size) maxSize := int64(100 * 1024 * 1024) // 100 MB if err := rotateLogFile(filename, maxSize); err != nil { log.Printf("Failed to rotate log file %s: %v", filename, err) // Continue anyway, rotation failure shouldn't stop logging } f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { log.Printf("Failed to create log file %s: %v, falling back to stdout", filename, err) return os.Stdout } return f } // New creates a new multi-file logger with separation of concerns func New(level string, format string, file string) *Logger { // Parse base filename for specialized logs baseDir := "logs" baseName := "mev_bot" if file != "" { // Extract directory and base filename parts := strings.Split(file, "/") if len(parts) > 1 { baseDir = strings.Join(parts[:len(parts)-1], "/") } filename := parts[len(parts)-1] if strings.Contains(filename, ".") { baseName = strings.Split(filename, ".")[0] } } // Create specialized log files mainFile := createLogFile(file) opportunityFile := createLogFile(fmt.Sprintf("%s/%s_opportunities.log", baseDir, baseName)) errorFile := createLogFile(fmt.Sprintf("%s/%s_errors.log", baseDir, baseName)) performanceFile := createLogFile(fmt.Sprintf("%s/%s_performance.log", baseDir, baseName)) transactionFile := createLogFile(fmt.Sprintf("%s/%s_transactions.log", baseDir, baseName)) // Create loggers with no prefixes (we format ourselves) logLevel := parseLogLevel(level) // Determine security level based on environment and log level var securityLevel SecurityLevel env := os.Getenv("GO_ENV") switch { case env == "production": securityLevel = SecurityLevelProduction case logLevel >= WARN: securityLevel = SecurityLevelInfo default: securityLevel = SecurityLevelDebug } return &Logger{ logger: log.New(mainFile, "", 0), opportunityLogger: log.New(opportunityFile, "", 0), errorLogger: log.New(errorFile, "", 0), performanceLogger: log.New(performanceFile, "", 0), transactionLogger: log.New(transactionFile, "", 0), level: logLevel, secureFilter: NewSecureFilter(securityLevel), levelName: level, } } // shouldLog determines if a message should be logged based on level func (l *Logger) shouldLog(level LogLevel) bool { return level >= l.level } // formatMessage formats a log message with timestamp and level func (l *Logger) formatMessage(level LogLevel, v ...interface{}) string { timestamp := time.Now().Format("2006/01/02 15:04:05") levelName := logLevelNames[level] message := formatKVMessage(v...) return fmt.Sprintf("%s [%s] %s", timestamp, levelName, message) } // formatKVMessage converts a variadic list of arguments into a structured log string. // It treats consecutive key/value pairs (string key followed by any value) specially // so that existing logger calls like logger.Error("msg", "key", value) render as // `msg key=value`. func formatKVMessage(args ...interface{}) string { if len(args) == 0 { return "" } var b strings.Builder // Always print the first argument verbatim to preserve legacy formatting. fmt.Fprintf(&b, "%v", args[0]) // Process subsequent arguments as key/value pairs where possible. for i := 1; i < len(args); i++ { key, ok := args[i].(string) if !ok || i == len(args)-1 { // Not a key/value pair, fall back to simple spacing. fmt.Fprintf(&b, " %v", args[i]) continue } value := args[i+1] fmt.Fprintf(&b, " %s=%v", key, value) i++ } return b.String() } // Debug logs a debug message func (l *Logger) Debug(v ...interface{}) { if l.shouldLog(DEBUG) { l.logger.Println(l.formatMessage(DEBUG, v...)) } } // Info logs an info message func (l *Logger) Info(v ...interface{}) { if l.shouldLog(INFO) { l.logger.Println(l.formatMessage(INFO, v...)) } } // Warn logs a warning message func (l *Logger) Warn(v ...interface{}) { if l.shouldLog(WARN) { message := l.formatMessage(WARN, v...) for _, substr := range suppressedWarningSubstrings { if strings.Contains(message, substr) { return } } l.logger.Println(message) l.errorLogger.Println(message) // Also log to error file } } // Error logs an error message func (l *Logger) Error(v ...interface{}) { if l.shouldLog(ERROR) { message := l.formatMessage(ERROR, v...) l.logger.Println(message) l.errorLogger.Println(message) // Also log to error file } } // Opportunity logs a found opportunity with detailed information // This always logs regardless of level since opportunities are critical func (l *Logger) Opportunity(txHash, from, to, method, protocol string, amountIn, amountOut, minOut, profitUSD float64, additionalData map[string]interface{}) { timestamp := time.Now().Format("2006/01/02 15:04:05") // Create sanitized additional data for production sanitizedData := l.secureFilter.SanitizeForProduction(additionalData) message := fmt.Sprintf(`%s [OPPORTUNITY] 🎯 ARBITRAGE OPPORTUNITY DETECTED ├── Transaction: %s ├── From: %s → To: %s ├── Method: %s (%s) ├── Amount In: %.6f tokens ├── Amount Out: %.6f tokens ├── Min Out: %.6f tokens ├── Estimated Profit: $%.2f USD └── Additional Data: %v`, timestamp, txHash, from, to, method, protocol, amountIn, amountOut, minOut, profitUSD, sanitizedData) // Apply security filtering to the entire message filteredMessage := l.secureFilter.FilterMessage(message) l.logger.Println(filteredMessage) l.opportunityLogger.Println(filteredMessage) // Dedicated opportunity log } // OpportunitySimple logs a simple opportunity message (for backwards compatibility) func (l *Logger) OpportunitySimple(v ...interface{}) { timestamp := time.Now().Format("2006/01/02 15:04:05") message := fmt.Sprintf("%s [OPPORTUNITY] %s", timestamp, fmt.Sprint(v...)) l.logger.Println(message) l.opportunityLogger.Println(message) // Dedicated opportunity log } // Performance logs performance metrics for optimization analysis func (l *Logger) Performance(component, operation string, duration time.Duration, metadata map[string]interface{}) { timestamp := time.Now().Format("2006/01/02 15:04:05") // Add standard performance fields data := map[string]interface{}{ "component": component, "operation": operation, "duration_ms": duration.Milliseconds(), "duration_ns": duration.Nanoseconds(), "timestamp": timestamp, } // Merge with provided metadata for k, v := range metadata { data[k] = v } message := fmt.Sprintf(`%s [PERFORMANCE] 📊 %s.%s completed in %v - %v`, timestamp, component, operation, duration, data) l.performanceLogger.Println(message) // Dedicated performance log only } // Metrics logs business metrics for analysis func (l *Logger) Metrics(name string, value float64, unit string, tags map[string]string) { timestamp := time.Now().Format("2006/01/02 15:04:05") message := fmt.Sprintf(`%s [METRICS] 📈 %s: %.6f %s %v`, timestamp, name, value, unit, tags) l.performanceLogger.Println(message) // Metrics go to performance log } // Transaction logs detailed transaction information for MEV analysis func (l *Logger) Transaction(txHash, from, to, method, protocol string, gasUsed, gasPrice uint64, value float64, success bool, metadata map[string]interface{}) { timestamp := time.Now().Format("2006/01/02 15:04:05") status := "FAILED" if success { status = "SUCCESS" } // Sanitize metadata for production sanitizedMetadata := l.secureFilter.SanitizeForProduction(metadata) message := fmt.Sprintf(`%s [TRANSACTION] 💳 %s ├── Hash: %s ├── From: %s → To: %s ├── Method: %s (%s) ├── Gas Used: %d (Price: %d wei) ├── Value: %.6f ETH ├── Status: %s └── Metadata: %v`, timestamp, status, txHash, from, to, method, protocol, gasUsed, gasPrice, value, status, sanitizedMetadata) // Apply security filtering to the entire message filteredMessage := l.secureFilter.FilterMessage(message) l.transactionLogger.Println(filteredMessage) // Dedicated transaction log only } // BlockProcessing logs block processing metrics for sequencer monitoring func (l *Logger) BlockProcessing(blockNumber uint64, txCount, dexTxCount int, processingTime time.Duration) { timestamp := time.Now().Format("2006/01/02 15:04:05") message := fmt.Sprintf(`%s [BLOCK_PROCESSING] 🧱 Block %d: %d txs (%d DEX) processed in %v`, timestamp, blockNumber, txCount, dexTxCount, processingTime) l.performanceLogger.Println(message) // Block processing metrics go to performance log } // ArbitrageAnalysis logs arbitrage opportunity analysis results func (l *Logger) ArbitrageAnalysis(poolA, poolB, tokenPair string, priceA, priceB, priceDiff, estimatedProfit float64, feasible bool) { timestamp := time.Now().Format("2006/01/02 15:04:05") status := "REJECTED" if feasible { status = "VIABLE" } message := fmt.Sprintf(`%s [ARBITRAGE_ANALYSIS] 🔍 %s %s ├── Pool A: %s (Price: %.6f) ├── Pool B: %s (Price: %.6f) ├── Price Difference: %.4f%% ├── Estimated Profit: $%.2f └── Status: %s`, timestamp, status, tokenPair, poolA, priceA, poolB, priceB, priceDiff*100, estimatedProfit, status) // Apply security filtering to protect sensitive pricing data filteredMessage := l.secureFilter.FilterMessage(message) l.opportunityLogger.Println(filteredMessage) // Arbitrage analysis goes to opportunity log } // RPC logs RPC call metrics for endpoint optimization func (l *Logger) RPC(endpoint, method string, duration time.Duration, success bool, errorMsg string) { timestamp := time.Now().Format("2006/01/02 15:04:05") status := "SUCCESS" if !success { status = "FAILED" } message := fmt.Sprintf(`%s [RPC] 🌐 %s %s.%s in %v`, timestamp, status, endpoint, method, duration) if !success && errorMsg != "" { message += fmt.Sprintf(" - Error: %s", errorMsg) } l.performanceLogger.Println(message) // RPC metrics go to performance log } // SwapAnalysis logs swap event analysis with security filtering func (l *Logger) SwapAnalysis(tokenIn, tokenOut string, amountIn, amountOut float64, protocol, poolAddr string, metadata map[string]interface{}) { timestamp := time.Now().Format("2006/01/02 15:04:05") // Sanitize metadata for production sanitizedMetadata := l.secureFilter.SanitizeForProduction(metadata) message := fmt.Sprintf(`%s [SWAP_ANALYSIS] 🔄 %s → %s ├── Amount In: %.6f %s ├── Amount Out: %.6f %s ├── Protocol: %s ├── Pool: %s └── Metadata: %v`, timestamp, tokenIn, tokenOut, amountIn, tokenIn, amountOut, tokenOut, protocol, poolAddr, sanitizedMetadata) // Apply security filtering to the entire message filteredMessage := l.secureFilter.FilterMessage(message) l.transactionLogger.Println(filteredMessage) // Dedicated transaction log } // rotateLogFile rotates a log file when it exceeds the maximum size func rotateLogFile(filename string, maxSize int64) error { // Check if file exists if _, err := os.Stat(filename); os.IsNotExist(err) { return nil // File doesn't exist, nothing to rotate } // Get file info fileInfo, err := os.Stat(filename) if err != nil { return fmt.Errorf("failed to get file info: %w", err) } // Check if file exceeds max size if fileInfo.Size() < maxSize { return nil // File is within size limits } // Create archive directory if it doesn't exist archiveDir := "logs/archived" if err := os.MkdirAll(archiveDir, 0755); err != nil { return fmt.Errorf("failed to create archive directory: %w", err) } // Generate archive filename with timestamp timestamp := time.Now().Format("20060102_150405") baseName := filepath.Base(filename) ext := filepath.Ext(baseName) name := strings.TrimSuffix(baseName, ext) archiveFilename := filepath.Join(archiveDir, fmt.Sprintf("%s_%s%s", name, timestamp, ext)) // Close current file handle and rename if err := os.Rename(filename, archiveFilename); err != nil { return fmt.Errorf("failed to rotate log file: %w", err) } return nil }