550 lines
17 KiB
Go
550 lines
17 KiB
Go
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"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
|
|
|
|
// FIX #25: Standardiser sur JSON en production/staging, console en développement
|
|
if env == "production" || env == "staging" {
|
|
config = zap.NewProductionConfig()
|
|
// En production/staging, utiliser JSON structuré pour faciliter l'agrégation
|
|
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)
|
|
|
|
// FIX #28: Ajouter sampling en production/staging pour éviter spam
|
|
// Initial: log les 100 premiers messages par seconde
|
|
// Thereafter: log 1 message toutes les 100 messages suivants
|
|
if env == "production" || env == "staging" {
|
|
config.Sampling = &zap.SamplingConfig{
|
|
Initial: 100,
|
|
Thereafter: 100,
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// FIX #25: Standardiser sur JSON en production/staging, console en développement
|
|
if env == "production" || env == "staging" {
|
|
config = zap.NewProductionConfig()
|
|
// En production/staging, utiliser JSON structuré pour faciliter l'agrégation
|
|
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)
|
|
|
|
// FIX #28: Ajouter sampling en production/staging pour éviter spam
|
|
// Initial: log les 100 premiers messages par seconde
|
|
// Thereafter: log 1 message toutes les 100 messages suivants
|
|
if env == "production" || env == "staging" {
|
|
config.Sampling = &zap.SamplingConfig{
|
|
Initial: 100,
|
|
Thereafter: 100,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
// FIX #25: Standardiser sur JSON en production/staging, console en développement
|
|
if env == "production" || env == "staging" {
|
|
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
|
|
|
|
// FIX #25: Standardiser sur JSON en production/staging, console en développement
|
|
if env == "production" || env == "staging" {
|
|
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
|
|
}
|
|
|
|
// NewLoggerWithFileRotation crée un logger avec rotation vers fichiers séparés
|
|
// - Tous les logs (DEBUG, INFO, WARN, ERROR) → module.log
|
|
// - Erreurs uniquement (ERROR) → module-error.log
|
|
// - Optionnellement stdout en développement
|
|
// logDir: répertoire des logs (ex: "/var/log/veza")
|
|
// moduleName: nom du module (ex: "backend-api", "chat", "stream")
|
|
// env: environnement ("production", "staging", "development")
|
|
// logLevel: niveau de log ("DEBUG", "INFO", "WARN", "ERROR")
|
|
func NewLoggerWithFileRotation(logDir, moduleName, env, logLevel string) (*Logger, error) {
|
|
var config zap.Config
|
|
|
|
// FIX #25: Standardiser sur JSON en production/staging, console en développement
|
|
if env == "production" || env == "staging" {
|
|
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)
|
|
|
|
// FIX #28: Ajouter sampling en production/staging pour éviter spam
|
|
if env == "production" || env == "staging" {
|
|
config.Sampling = &zap.SamplingConfig{
|
|
Initial: 100,
|
|
Thereafter: 100,
|
|
}
|
|
}
|
|
|
|
// Créer le répertoire de logs s'il n'existe pas
|
|
// En développement, utiliser un répertoire local si /var/log n'est pas accessible
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
// En développement, fallback vers un répertoire local
|
|
if env == "development" || env == "dev" {
|
|
fallbackDir := "./logs"
|
|
if err2 := os.MkdirAll(fallbackDir, 0755); err2 != nil {
|
|
return nil, fmt.Errorf("failed to create log directory %s (fallback %s also failed: %v): %w", logDir, fallbackDir, err2, err)
|
|
}
|
|
logDir = fallbackDir
|
|
} else {
|
|
return nil, fmt.Errorf("failed to create log directory %s: %w (hint: create it manually with 'sudo mkdir -p %s && sudo chown $USER:$USER %s')", logDir, err, logDir, logDir)
|
|
}
|
|
}
|
|
|
|
cores := []zapcore.Core{}
|
|
|
|
// Core 1: Tous les logs vers module.log
|
|
allLogsFile := fmt.Sprintf("%s/%s.log", logDir, moduleName)
|
|
allLogsWriter := &lumberjack.Logger{
|
|
Filename: allLogsFile,
|
|
MaxSize: 100, // MB
|
|
MaxBackups: 10,
|
|
MaxAge: 30, // jours
|
|
Compress: true,
|
|
}
|
|
allLogsBuffered := createBufferedAsyncWriter(allLogsWriter)
|
|
allLogsCore := zapcore.NewCore(
|
|
zapcore.NewJSONEncoder(config.EncoderConfig),
|
|
zapcore.AddSync(allLogsBuffered),
|
|
level,
|
|
)
|
|
cores = append(cores, allLogsCore)
|
|
|
|
// Core 2: Erreurs uniquement vers module-error.log
|
|
errorLogsFile := fmt.Sprintf("%s/%s-error.log", logDir, moduleName)
|
|
errorLogsWriter := &lumberjack.Logger{
|
|
Filename: errorLogsFile,
|
|
MaxSize: 100, // MB
|
|
MaxBackups: 10,
|
|
MaxAge: 30, // jours
|
|
Compress: true,
|
|
}
|
|
errorLogsBuffered := createBufferedAsyncWriter(errorLogsWriter)
|
|
errorLogsCore := zapcore.NewCore(
|
|
zapcore.NewJSONEncoder(config.EncoderConfig),
|
|
zapcore.AddSync(errorLogsBuffered),
|
|
zapcore.ErrorLevel, // Seulement les erreurs
|
|
)
|
|
cores = append(cores, errorLogsCore)
|
|
|
|
// Core 3: stdout en développement pour debugging
|
|
if env == "development" || env == "dev" {
|
|
stdoutCore := zapcore.NewCore(
|
|
zapcore.NewConsoleEncoder(config.EncoderConfig),
|
|
zapcore.AddSync(os.Stdout),
|
|
level,
|
|
)
|
|
cores = append(cores, stdoutCore)
|
|
}
|
|
|
|
// Combiner tous les cores
|
|
core := zapcore.NewTee(cores...)
|
|
|
|
// Créer le logger
|
|
logger := zap.New(core,
|
|
zap.AddCaller(),
|
|
zap.AddStacktrace(zapcore.ErrorLevel),
|
|
)
|
|
|
|
return &Logger{zap: logger}, nil
|
|
}
|