package logging import ( "io" "os" "runtime" "strings" "github.com/centrifugal/centrifugo/v6/internal/config" "github.com/centrifugal/centrifugo/v6/internal/logutils" "github.com/centrifugal/centrifuge" "github.com/mattn/go-isatty" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) type Level = zerolog.Level const ( TraceLevel = zerolog.TraceLevel DebugLevel = zerolog.DebugLevel InfoLevel = zerolog.InfoLevel WarnLevel = zerolog.WarnLevel ErrorLevel = zerolog.ErrorLevel ) var logLevelMatches = map[string]zerolog.Level{ "NONE": zerolog.Disabled, "TRACE": zerolog.TraceLevel, "DEBUG": zerolog.DebugLevel, "INFO": zerolog.InfoLevel, "WARN": zerolog.WarnLevel, "ERROR": zerolog.ErrorLevel, "FATAL": zerolog.FatalLevel, } func CentrifugeLogLevel(level string) centrifuge.LogLevel { if l, ok := logStringToLevel[strings.ToLower(level)]; ok { return l } return centrifuge.LogLevelInfo } // logStringToLevel matches level string to Centrifuge LogLevel. var logStringToLevel = map[string]centrifuge.LogLevel{ "trace": centrifuge.LogLevelTrace, "debug": centrifuge.LogLevelDebug, "info": centrifuge.LogLevelInfo, "error": centrifuge.LogLevelError, "none": centrifuge.LogLevelNone, } // centrifugeLogHandlerBufferSize is the number of log entries buffered between // the (many) goroutines producing them and the single goroutine writing them // out. Handle drops entries when this is full, so the buffer has to absorb // bursts rather than merely smooth a steady rate. // // Bursts are exactly when the logs matter. A Redis failover, for instance, makes // every shard's client and control PUB/SUB loop log connection errors, subscribe // errors and lifecycle transitions at nearly the same instant, while the drain // goroutine is rate-limited by writing to stdout. The previous value of 64 was // overrun easily in that situation — and with log_level debug, which is what an // operator turns on to investigate, it fills faster still. const centrifugeLogHandlerBufferSize = 1024 type centrifugeLogHandler struct { entries chan centrifuge.LogEntry } func newCentrifugeLogHandler() *centrifugeLogHandler { h := ¢rifugeLogHandler{ entries: make(chan centrifuge.LogEntry, centrifugeLogHandlerBufferSize), } go h.readEntries() return h } func (h *centrifugeLogHandler) readEntries() { for entry := range h.entries { var l *zerolog.Event switch entry.Level { case centrifuge.LogLevelTrace: l = log.Trace() case centrifuge.LogLevelDebug: l = log.Debug() case centrifuge.LogLevelInfo: l = log.Info() case centrifuge.LogLevelWarn: l = log.Warn() case centrifuge.LogLevelError: l = log.Error() default: continue } if entry.Fields != nil { if entry.Error != nil { l = l.Err(entry.Error) delete(entry.Fields, "error") } l.Fields(entry.Fields).Msg(entry.Message) } else { if entry.Error != nil { l = l.Err(entry.Error) delete(entry.Fields, "error") } l.Msg(entry.Message) } } } func (h *centrifugeLogHandler) Handle(entry centrifuge.LogEntry) { select { case h.entries <- entry: default: return } } func configureConsoleWriter() *zerolog.ConsoleWriter { if isTerminalAttached() { return &zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: "2006-01-02 15:04:05", FormatLevel: logutils.ConsoleFormatLevel(), FormatErrFieldName: logutils.ConsoleFormatErrFieldName(), FormatErrFieldValue: logutils.ConsoleFormatErrFieldValue(), } } return nil } func isTerminalAttached() bool { return isatty.IsTerminal(os.Stdout.Fd()) && runtime.GOOS != "windows" } func Setup(cfg config.Config) (centrifuge.LogHandler, func()) { var writers []io.Writer var file *os.File if cfg.Log.File != "" { var err error file, err = os.OpenFile(cfg.Log.File, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) //nolint:gosec // Common for logging. if err != nil { log.Fatal().Err(err).Msg("error opening log file") } writers = append(writers, file) } else { consoleWriter := configureConsoleWriter() if consoleWriter != nil { writers = append(writers, consoleWriter) } else { writers = append(writers, os.Stderr) } } levelFound := true logLevel, ok := logLevelMatches[strings.ToUpper(cfg.Log.Level)] if !ok { levelFound = false logLevel = zerolog.InfoLevel } zerolog.SetGlobalLevel(logLevel) if len(writers) > 0 { mw := io.MultiWriter(writers...) log.Logger = log.Output(mw) } if !levelFound { log.Warn().Msgf("unknown log level %s, defaulting to INFO", cfg.Log.Level) } return newCentrifugeLogHandler().Handle, func() { if file != nil { _ = file.Close() } } } // Enabled checks if a specific logging level is enabled func Enabled(level Level) bool { return level >= zerolog.GlobalLevel() }