package main import ( "fmt" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/zapjournald" "github.com/spf13/viper" "github.com/ssgreg/journald" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( destinationStdout string = "stdout" destinationJournald string = "journald" ) var defaultTags = []string{logs.TagApp, logs.TagDatapath, logs.TagExternalStorage, logs.TagExternalStorageTree, logs.TagExternalBlockchain} type Logger struct { logger *zap.Logger lvl zap.AtomicLevel } func pickLogger(v *viper.Viper, loggerSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger { dest := v.GetString(cfgLoggerDestination) switch dest { case destinationStdout: return newStdoutLogger(v, loggerSettings, tagSettings) case destinationJournald: return newJournaldLogger(v, loggerSettings, tagSettings) default: panic(fmt.Sprintf("wrong destination for logger: %s", dest)) } } // newStdoutLogger constructs a Logger instance for the current application. // Panics on failure. // // Logger contains a logger is built from zap's production logging configuration with: // - parameterized level (debug by default) // - console encoding // - ISO8601 time encoding // - sampling intervals // // and atomic log level to dynamically change it. // // Logger records a stack trace for all messages at or above fatal level. // // See also zapcore.Level, zap.NewProductionConfig, zap.AddStacktrace. func newStdoutLogger(v *viper.Viper, loggerSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger { c := newZapLogConfig(v) out, errSink, err := openZapSinks(c) if err != nil { panic(fmt.Sprintf("open zap sinks: %v", err.Error())) } core := zapcore.NewCore(zapcore.NewConsoleEncoder(c.EncoderConfig), out, c.Level) core = applyZapCoreMiddlewares(core, v, loggerSettings, tagSettings) l := zap.New(core, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)), zap.ErrorOutput(errSink)) return &Logger{logger: l, lvl: c.Level} } func newJournaldLogger(v *viper.Viper, logSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger { c := newZapLogConfig(v) // We can use NewJSONEncoder instead if, say, frontend // would like to access journald logs and parse them easily. encoder := zapjournald.NewPartialEncoder(zapcore.NewConsoleEncoder(c.EncoderConfig), zapjournald.SyslogFields) journalCore := zapjournald.NewCore(c.Level, encoder, &journald.Journal{}, zapjournald.SyslogFields) core := journalCore.With([]zapcore.Field{ zapjournald.SyslogFacility(zapjournald.LogDaemon), zapjournald.SyslogIdentifier(), zapjournald.SyslogPid(), }) core = applyZapCoreMiddlewares(core, v, logSettings, tagSettings) l := zap.New(core, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel))) return &Logger{logger: l, lvl: c.Level} } func openZapSinks(cfg zap.Config) (zapcore.WriteSyncer, zapcore.WriteSyncer, error) { sink, closeOut, err := zap.Open(cfg.OutputPaths...) if err != nil { return nil, nil, err } errSink, _, err := zap.Open(cfg.ErrorOutputPaths...) if err != nil { closeOut() return nil, nil, err } return sink, errSink, nil } var _ zapcore.Core = (*zapCoreTagFilterWrapper)(nil) type zapCoreTagFilterWrapper struct { core zapcore.Core settings TagFilterSettings extra []zap.Field } type TagFilterSettings interface { LevelEnabled(tag string, lvl zapcore.Level) bool } func (c *zapCoreTagFilterWrapper) Enabled(level zapcore.Level) bool { return c.core.Enabled(level) } func (c *zapCoreTagFilterWrapper) With(fields []zapcore.Field) zapcore.Core { return &zapCoreTagFilterWrapper{ core: c.core.With(fields), settings: c.settings, extra: append(c.extra, fields...), } } func (c *zapCoreTagFilterWrapper) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { if c.core.Enabled(entry.Level) { return checked.AddCore(entry, c) } return checked } func (c *zapCoreTagFilterWrapper) Write(entry zapcore.Entry, fields []zapcore.Field) error { if c.shouldSkip(entry, fields) || c.shouldSkip(entry, c.extra) { return nil } return c.core.Write(entry, fields) } func (c *zapCoreTagFilterWrapper) shouldSkip(entry zapcore.Entry, fields []zap.Field) bool { for _, field := range fields { if field.Key == logs.TagFieldName && field.Type == zapcore.StringType { if !c.settings.LevelEnabled(field.String, entry.Level) { return true } break } } return false } func (c *zapCoreTagFilterWrapper) Sync() error { return c.core.Sync() } func applyZapCoreMiddlewares(core zapcore.Core, v *viper.Viper, appSettings LoggerAppSettings, tagSettings TagFilterSettings) zapcore.Core { core = &zapCoreTagFilterWrapper{ core: core, settings: tagSettings, } if v.GetBool(cfgLoggerSamplingEnabled) { core = zapcore.NewSamplerWithOptions(core, v.GetDuration(cfgLoggerSamplingInterval), v.GetInt(cfgLoggerSamplingInitial), v.GetInt(cfgLoggerSamplingThereafter), zapcore.SamplerHook(func(_ zapcore.Entry, dec zapcore.SamplingDecision) { if dec&zapcore.LogDropped > 0 { appSettings.DroppedLogsInc() } })) } return core } func newZapLogConfig(v *viper.Viper) zap.Config { lvl, err := getLogLevel(v) if err != nil { panic(err) } c := zap.Config{ Level: zap.NewAtomicLevelAt(lvl), EncoderConfig: zap.NewProductionEncoderConfig(), OutputPaths: []string{"stderr"}, ErrorOutputPaths: []string{"stderr"}, } c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder return c } func getLogLevel(v *viper.Viper) (zapcore.Level, error) { var lvl zapcore.Level lvlStr := v.GetString(cfgLoggerLevel) err := lvl.UnmarshalText([]byte(lvlStr)) if err != nil { return lvl, fmt.Errorf("incorrect logger level configuration %s (%v), "+ "value should be one of %v", lvlStr, err, [...]zapcore.Level{ zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel, zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel, }) } return lvl, nil }