package logger import ( "io" "os" "path/filepath" "regexp" "sync" "github.com/sirupsen/logrus" ) var ( Log *logrus.Logger once sync.Once mu sync.RWMutex ) // Config holds logging configuration type Config struct { Level string `yaml:"level"` // debug, info, warn, error Format string `yaml:"format"` // json, text File string `yaml:"file"` // log file path (empty for stdout) MaxSize int `yaml:"max_size"` // max log file size in MB MaxBackups int `yaml:"max_backups"` // max number of backup files MaxAge int `yaml:"max_age"` // max age of backup files in days Compress bool `yaml:"compress"` // compress backup files } // Init initializes the global logger with the given configuration func Init(config Config) error { mu.Lock() defer mu.Unlock() Log = logrus.New() // Set log level level, err := logrus.ParseLevel(config.Level) if err != nil { level = logrus.InfoLevel } Log.SetLevel(level) // Set log format with sanitization switch config.Format { case "json": Log.SetFormatter(&SanitizedJSONFormatter{ TimestampFormat: "2006-01-02 15:04:05", }) default: Log.SetFormatter(&SanitizedTextFormatter{ FullTimestamp: true, TimestampFormat: "2006-01-02 15:04:05", }) } // Set output if config.File != "" { // Ensure directory exists dir := filepath.Dir(config.File) if err := os.MkdirAll(dir, 0755); err != nil { return err } // Open log file with secure permissions (owner read/write only) file, err := os.OpenFile(config.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err } // Set output to both file and stdout Log.SetOutput(io.MultiWriter(file, os.Stdout)) } else { Log.SetOutput(os.Stdout) } return nil } // GetLogger returns the global logger instance func GetLogger() *logrus.Logger { mu.RLock() if Log != nil { mu.RUnlock() return Log } mu.RUnlock() // Initialize with default config if not already initialized once.Do(func() { Init(Config{ Level: "info", Format: "text", File: "", }) }) mu.RLock() defer mu.RUnlock() return Log } // Helper functions for common logging operations func Debug(args ...interface{}) { GetLogger().Debug(args...) } func Debugf(format string, args ...interface{}) { GetLogger().Debugf(format, args...) } func Info(args ...interface{}) { GetLogger().Info(args...) } func Infof(format string, args ...interface{}) { GetLogger().Infof(format, args...) } func Warn(args ...interface{}) { GetLogger().Warn(args...) } func Warnf(format string, args ...interface{}) { GetLogger().Warnf(format, args...) } func Error(args ...interface{}) { GetLogger().Error(args...) } func Errorf(format string, args ...interface{}) { GetLogger().Errorf(format, args...) } func Fatal(args ...interface{}) { GetLogger().Fatal(args...) } func Fatalf(format string, args ...interface{}) { GetLogger().Fatalf(format, args...) } // WithField creates a new logger entry with a field func WithField(key string, value interface{}) *logrus.Entry { return GetLogger().WithField(key, value) } // WithFields creates a new logger entry with multiple fields func WithFields(fields logrus.Fields) *logrus.Entry { return GetLogger().WithFields(fields) } // SanitizedTextFormatter sanitizes log output type SanitizedTextFormatter struct { FullTimestamp bool TimestampFormat string } // Format formats the log entry with sanitization func (f *SanitizedTextFormatter) Format(entry *logrus.Entry) ([]byte, error) { // Sanitize sensitive data sanitizedEntry := *entry sanitizedEntry.Message = sanitizeString(entry.Message) // Sanitize fields sanitizedFields := make(logrus.Fields) for k, v := range entry.Data { sanitizedFields[k] = sanitizeValue(v) } sanitizedEntry.Data = sanitizedFields // Use default text formatter formatter := &logrus.TextFormatter{ FullTimestamp: f.FullTimestamp, TimestampFormat: f.TimestampFormat, } return formatter.Format(&sanitizedEntry) } // SanitizedJSONFormatter sanitizes JSON log output type SanitizedJSONFormatter struct { TimestampFormat string } // Format formats the log entry with sanitization func (f *SanitizedJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) { // Sanitize sensitive data sanitizedEntry := *entry sanitizedEntry.Message = sanitizeString(entry.Message) // Sanitize fields sanitizedFields := make(logrus.Fields) for k, v := range entry.Data { sanitizedFields[k] = sanitizeValue(v) } sanitizedEntry.Data = sanitizedFields // Use default JSON formatter formatter := &logrus.JSONFormatter{ TimestampFormat: f.TimestampFormat, } return formatter.Format(&sanitizedEntry) } // sanitizeString removes or masks sensitive information func sanitizeString(s string) string { // Remove potential encryption keys (hex strings longer than 32 chars) keyPattern := regexp.MustCompile(`[a-fA-F0-9]{32,}`) s = keyPattern.ReplaceAllString(s, "[REDACTED_KEY]") // Remove potential passwords and tokens passwordPattern := regexp.MustCompile(`(?i)(password|pass|pwd|secret|key|token|auth|credential)\s*[:=]\s*[^\s]+`) s = passwordPattern.ReplaceAllString(s, "$1=[REDACTED]") // Remove potential API keys and tokens apiKeyPattern := regexp.MustCompile(`(?i)(api[_-]?key|access[_-]?token|bearer[_-]?token)\s*[:=]\s*[^\s]+`) s = apiKeyPattern.ReplaceAllString(s, "$1=[REDACTED]") // Remove potential database connection strings dbPattern := regexp.MustCompile(`(?i)(mysql|postgres|mongodb|redis)://[^@]+@[^\s]+`) s = dbPattern.ReplaceAllString(s, "[REDACTED_DB_CONNECTION]") // Remove potential JWT tokens jwtPattern := regexp.MustCompile(`eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*`) s = jwtPattern.ReplaceAllString(s, "[REDACTED_JWT]") // Remove potential credit card numbers ccPattern := regexp.MustCompile(`\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b`) s = ccPattern.ReplaceAllString(s, "[REDACTED_CC]") // Remove potential SSNs ssnPattern := regexp.MustCompile(`\b\d{3}-\d{2}-\d{4}\b`) s = ssnPattern.ReplaceAllString(s, "[REDACTED_SSN]") // Remove potential IP addresses in sensitive contexts ipPattern := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b`) s = ipPattern.ReplaceAllString(s, "[REDACTED_IP]") // Remove potential email addresses emailPattern := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`) s = emailPattern.ReplaceAllString(s, "[REDACTED_EMAIL]") return s } // sanitizeValue sanitizes various value types func sanitizeValue(v interface{}) interface{} { switch val := v.(type) { case string: return sanitizeString(val) case []byte: return "[BINARY_DATA]" case map[string]interface{}: sanitized := make(map[string]interface{}) for k, v := range val { sanitized[k] = sanitizeValue(v) } return sanitized case []interface{}: sanitized := make([]interface{}, len(val)) for i, v := range val { sanitized[i] = sanitizeValue(v) } return sanitized default: return v } }