// Package logger define log struct of modelRT project package logger import ( "fmt" "os" "sync" "time" "modelRT/config" "modelRT/constants" "github.com/natefinch/lumberjack" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var ( once sync.Once _globalLoggerMu sync.RWMutex _globalLogger *zap.Logger ) // getEncoder returns a console encoder for development (human-readable, colored) and a JSON encoder // for container modes (parseable by Promtail pipeline_stages). func getEncoder(mode string) zapcore.Encoder { cfg := zap.NewProductionEncoderConfig() cfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") cfg.TimeKey = "time" cfg.EncodeCaller = zapcore.ShortCallerEncoder if mode == constants.DevelopmentLogMode { cfg.EncodeLevel = zapcore.CapitalColorLevelEncoder return zapcore.NewConsoleEncoder(cfg) } cfg.EncodeLevel = zapcore.CapitalLevelEncoder return zapcore.NewJSONEncoder(cfg) } // getWriteSyncer returns write targets based on mode: // - development: stdout + optional Loki direct-push (when loki.endpoint is set) // - container modes: stdout always (Promtail collects) + rotating file (when filepath is set) func getWriteSyncer(lCfg config.LoggerConfig) zapcore.WriteSyncer { stdout := zapcore.AddSync(os.Stdout) if lCfg.Mode == constants.DevelopmentLogMode { if lCfg.Loki.Endpoint == "" { return stdout } return zapcore.NewMultiWriteSyncer(stdout, newLokiSyncer(lCfg.Loki)) } syncers := []zapcore.WriteSyncer{stdout} if lCfg.FilePath != "" { dateStr := time.Now().Format("2006-01-02 15:04:05") syncers = append(syncers, zapcore.AddSync(&lumberjack.Logger{ Filename: fmt.Sprintf(lCfg.FilePath, dateStr), MaxSize: lCfg.MaxSize, MaxAge: lCfg.MaxAge, MaxBackups: lCfg.MaxBackups, Compress: lCfg.Compress, })) } return zapcore.NewMultiWriteSyncer(syncers...) } // containerFields reads K8s Downward API environment variables and returns them as global zap fields. // These fields appear on every log line, allowing Loki/Grafana to filter by pod, namespace, and node. // Inject them in the Deployment manifest: // // env: // - name: K8S_NAMESPACE // valueFrom: {fieldRef: {fieldPath: metadata.namespace}} // - name: K8S_NODE_NAME // valueFrom: {fieldRef: {fieldPath: spec.nodeName}} func containerFields() []zap.Field { var fields []zap.Field // HOSTNAME is automatically set to the pod name by Kubernetes. if pod := os.Getenv("HOSTNAME"); pod != "" { fields = append(fields, zap.String("pod", pod)) } if ns := os.Getenv("K8S_NAMESPACE"); ns != "" { fields = append(fields, zap.String("namespace", ns)) } if node := os.Getenv("K8S_NODE_NAME"); node != "" { fields = append(fields, zap.String("node", node)) } return fields } // initLogger return successfully initialized zap logger func initLogger(lCfg config.LoggerConfig) *zap.Logger { writeSyncer := getWriteSyncer(lCfg) encoder := getEncoder(lCfg.Mode) l := new(zapcore.Level) if err := l.UnmarshalText([]byte(lCfg.Level)); err != nil { panic(err) } core := zapcore.NewCore(encoder, writeSyncer, l) opts := []zap.Option{zap.AddCaller()} if lCfg.Mode != constants.DevelopmentLogMode { opts = append(opts, zap.Fields(containerFields()...)) } logger := zap.New(core, opts...) zap.ReplaceGlobals(logger) return logger } // InitLoggerInstance define func of return instance of zap logger func InitLoggerInstance(lCfg config.LoggerConfig) { once.Do(func() { _globalLogger = initLogger(lCfg) }) } // GetLoggerInstance define func of returns the global logger instance It's safe for concurrent use. func GetLoggerInstance() *zap.Logger { _globalLoggerMu.RLock() logger := _globalLogger _globalLoggerMu.RUnlock() return logger }