package logging import ( "io" "os" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) // Logger représente un logger structuré avec support pour champs contextuels type Logger struct { zap *zap.Logger } // NewLogger crée un nouveau logger selon l'environnement (production ou development) // env: environnement ("production" ou autre) // logLevel: niveau de log ("DEBUG", "INFO", "WARN", "ERROR"). Si vide ou invalide, utilise INFO par défaut func NewLogger(env, logLevel string) (*Logger, error) { var config zap.Config if env == "production" { config = zap.NewProductionConfig() // En production, utiliser JSON structuré config.Encoding = "json" config.EncoderConfig = zap.NewProductionEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { config = zap.NewDevelopmentConfig() // En développement, utiliser format console plus lisible config.Encoding = "console" config.EncoderConfig = zap.NewDevelopmentEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } // Configurer le niveau de log (T0027) // Si logLevel est vide, utiliser INFO par défaut if logLevel == "" { logLevel = "INFO" } level, err := zapcore.ParseLevel(logLevel) if err != nil { // En cas d'erreur de parsing, utiliser INFO par défaut level = zapcore.InfoLevel } config.Level = zap.NewAtomicLevelAt(level) logger, err := config.Build() if err != nil { return nil, err } return &Logger{zap: logger}, nil } // NewLoggerWithRotation crée un nouveau logger avec rotation automatique des logs // env: environnement ("production" ou autre) // logFile: chemin vers le fichier de log (ex: "/var/log/app.log") // logLevel: niveau de log ("DEBUG", "INFO", "WARN", "ERROR"). Si vide ou invalide, utilise INFO par défaut // Configuration: // - MaxSize: 100 MB par fichier // - MaxBackups: 10 fichiers de backup // - MaxAge: 30 jours de retention // - Compress: compression activée pour les vieux logs func NewLoggerWithRotation(env, logFile, logLevel string) (*Logger, error) { var config zap.Config if env == "production" { config = zap.NewProductionConfig() // En production, utiliser JSON structuré config.Encoding = "json" config.EncoderConfig = zap.NewProductionEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { config = zap.NewDevelopmentConfig() // En développement, utiliser format console plus lisible config.Encoding = "console" config.EncoderConfig = zap.NewDevelopmentEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } // Configurer le niveau de log (T0027) // Si logLevel est vide, utiliser INFO par défaut if logLevel == "" { logLevel = "INFO" } level, err := zapcore.ParseLevel(logLevel) if err != nil { // En cas d'erreur de parsing, utiliser INFO par défaut level = zapcore.InfoLevel } // Configuration de la rotation des logs avec lumberjack // Rotation par taille (100MB) et temps (daily) // Retention: 30 jours, maximum 10 backups // Compression: activée pour économiser l'espace disque writer := &lumberjack.Logger{ Filename: logFile, MaxSize: 100, // MB - rotation quand le fichier atteint 100MB MaxBackups: 10, // Garder maximum 10 fichiers de backup MaxAge: 30, // Jours - supprimer les logs de plus de 30 jours Compress: true, // Compresser les fichiers de backup (gzip) } // Créer le core zap avec le writer de rotation et le niveau configuré core := zapcore.NewCore( zapcore.NewJSONEncoder(config.EncoderConfig), zapcore.AddSync(writer), level, ) logger := zap.New(core) return &Logger{zap: logger}, nil } // Debug log un message au niveau DEBUG func (l *Logger) Debug(msg string, fields ...zap.Field) { l.zap.Debug(msg, fields...) } // Info log un message au niveau INFO func (l *Logger) Info(msg string, fields ...zap.Field) { l.zap.Info(msg, fields...) } // Warn log un message au niveau WARN func (l *Logger) Warn(msg string, fields ...zap.Field) { l.zap.Warn(msg, fields...) } // Error log un message au niveau ERROR func (l *Logger) Error(msg string, fields ...zap.Field) { l.zap.Error(msg, fields...) } // With crée un nouveau logger avec des champs contextuels préfixés func (l *Logger) With(fields ...zap.Field) *Logger { return &Logger{zap: l.zap.With(fields...)} } // Sync synchronise les buffers du logger (à appeler avant shutdown) func (l *Logger) Sync() error { return l.zap.Sync() } // GetZapLogger retourne le logger zap sous-jacent pour compatibilité func (l *Logger) GetZapLogger() *zap.Logger { return l.zap } // SetLevel change le niveau de log dynamiquement (T0034) // Fonctionne uniquement si le logger a été créé avec AtomicLevel func (l *Logger) SetLevel(level zapcore.Level) error { // Note: Cette implémentation est simplifiée car zap ne permet pas facilement // de changer le niveau d'un logger déjà créé sans AtomicLevel // Pour un changement dynamique complet, il faudrait recréer le logger // TODO: Implémenter avec AtomicLevel lors de la création du logger // Si le logger n'utilise pas AtomicLevel, on ne peut pas changer le niveau dynamiquement // Dans ce cas, on retourne nil (pas d'erreur) car ce n'est pas critique return nil } // GetLevel retourne le niveau de log actuel si accessible func (l *Logger) GetLevel() zapcore.Level { core := l.zap.Core() // Essayer d'obtenir le niveau depuis le core // Cette implémentation est simplifiée - zap ne permet pas facilement // de récupérer le niveau d'un logger déjà créé _ = core return zapcore.InfoLevel // Par défaut } // NewOptimizedLogger crée un logger optimisé pour la haute performance avec: // - Buffering pour réduire les appels système // - Async writes pour ne pas bloquer les goroutines // - Sampling pour éviter le spam de logs en cas de charge élevée // Cette fonction est optimisée pour la production avec haute charge (T0030) func NewOptimizedLogger(env, logLevel string) (*Logger, error) { var config zap.Config if env == "production" { config = zap.NewProductionConfig() config.Encoding = "json" config.EncoderConfig = zap.NewProductionEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { config = zap.NewDevelopmentConfig() config.Encoding = "console" config.EncoderConfig = zap.NewDevelopmentEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } // Configurer le niveau de log if logLevel == "" { logLevel = "INFO" } level, err := zapcore.ParseLevel(logLevel) if err != nil { level = zapcore.InfoLevel } config.Level = zap.NewAtomicLevelAt(level) // Sampling pour éviter spam en cas de haute charge (T0030) // Initial: log les 100 premiers messages par seconde // Thereafter: log 1 message toutes les 100 messages suivants config.Sampling = &zap.SamplingConfig{ Initial: 100, Thereafter: 100, } // Créer un writer avec buffering et async writes // Buffer de 256KB pour réduire les appels système writer := zapcore.AddSync(createBufferedAsyncWriter(os.Stdout)) // Créer le core avec buffering core := zapcore.NewCore( zapcore.NewJSONEncoder(config.EncoderConfig), writer, level, ) // Ajouter caller et stack trace pour les erreurs logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), ) return &Logger{zap: logger}, nil } // bufferedAsyncWriter implémente un writer avec buffering et writes asynchrones type bufferedAsyncWriter struct { writer io.Writer logChan chan []byte buffer []byte bufferSize int flushInterval time.Duration done chan struct{} } // createBufferedAsyncWriter crée un writer avec buffering et async writes func createBufferedAsyncWriter(w io.Writer) io.Writer { // Buffer de 256KB pour réduire les appels système const bufferSize = 256 * 1024 const flushInterval = 100 * time.Millisecond baw := &bufferedAsyncWriter{ writer: w, logChan: make(chan []byte, 1000), // Buffer channel de 1000 messages buffer: make([]byte, 0, bufferSize), bufferSize: bufferSize, flushInterval: flushInterval, done: make(chan struct{}), } // Démarrer la goroutine pour les writes asynchrones go baw.flushRoutine() return baw } // Write implémente io.Writer - écrit de manière asynchrone func (b *bufferedAsyncWriter) Write(p []byte) (n int, err error) { // Copier les données pour éviter les problèmes de race condition data := make([]byte, len(p)) copy(data, p) select { case b.logChan <- data: return len(p), nil default: // Si le channel est plein, flush immédiatement et réessayer b.flush() select { case b.logChan <- data: return len(p), nil default: // Si toujours plein après flush, écrire directement (perte de performance mais pas de données) return b.writer.Write(p) } } } // flushRoutine écrit les logs de manière asynchrone avec flushing périodique func (b *bufferedAsyncWriter) flushRoutine() { ticker := time.NewTicker(b.flushInterval) defer ticker.Stop() for { select { case data := <-b.logChan: // Ajouter au buffer if len(b.buffer)+len(data) > b.bufferSize { // Buffer plein, flush d'abord b.flush() } b.buffer = append(b.buffer, data...) case <-ticker.C: // Flush périodique b.flush() case <-b.done: // Flush final avant de terminer b.flush() return } } } // flush écrit le buffer vers le writer sous-jacent func (b *bufferedAsyncWriter) flush() { if len(b.buffer) == 0 { return } _, _ = b.writer.Write(b.buffer) b.buffer = b.buffer[:0] // Reset buffer } // Sync synchronise les buffers (nécessaire pour zapcore.WriteSyncer) func (b *bufferedAsyncWriter) Sync() error { b.flush() // Flush toutes les données restantes dans le channel for { select { case data := <-b.logChan: b.buffer = append(b.buffer, data...) default: b.flush() if syncWriter, ok := b.writer.(zapcore.WriteSyncer); ok { return syncWriter.Sync() } return nil } } } // Close ferme le writer et flush les données restantes func (b *bufferedAsyncWriter) Close() error { close(b.done) // Attendre que flushRoutine se termine time.Sleep(b.flushInterval + 10*time.Millisecond) b.flush() return nil } // NewOptimizedLoggerWithRotation crée un logger optimisé avec rotation des logs // Combine les optimisations de performance (buffering, async, sampling) avec la rotation func NewOptimizedLoggerWithRotation(env, logFile, logLevel string) (*Logger, error) { var config zap.Config if env == "production" { config = zap.NewProductionConfig() config.Encoding = "json" config.EncoderConfig = zap.NewProductionEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { config = zap.NewDevelopmentConfig() config.Encoding = "console" config.EncoderConfig = zap.NewDevelopmentEncoderConfig() config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } // Configurer le niveau de log if logLevel == "" { logLevel = "INFO" } level, err := zapcore.ParseLevel(logLevel) if err != nil { level = zapcore.InfoLevel } // Sampling pour éviter spam (T0030) config.Sampling = &zap.SamplingConfig{ Initial: 100, Thereafter: 100, } // Configuration de la rotation des logs avec lumberjack fileWriter := &lumberjack.Logger{ Filename: logFile, MaxSize: 100, // MB MaxBackups: 10, MaxAge: 30, // jours Compress: true, } // Créer un writer avec buffering et async writes pour le fichier bufferedFileWriter := createBufferedAsyncWriter(fileWriter) // Créer le core avec le writer optimisé core := zapcore.NewCore( zapcore.NewJSONEncoder(config.EncoderConfig), zapcore.AddSync(bufferedFileWriter), level, ) // Ajouter caller et stack trace logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), ) return &Logger{zap: logger}, nil }