package logger import ( "fmt" "sync/atomic" "time" "git.frostfs.info/TrueCloudLab/zapjournald" "github.com/ssgreg/journald" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Logger represents a component // for writing messages to log. type Logger struct { z *zap.Logger lvl zap.AtomicLevel // Tag used by Logger t Tag // Contains map of Tag to log level, overrides lvl tl *atomic.Value // Parent zap.Logger, required to override field zapTagFieldName in the output pz *zap.Logger } // Prm groups Logger's parameters. // Successful passing non-nil parameters to the NewLogger (if returned // error is nil) connects the parameters with the returned Logger. // Parameters that have been connected to the Logger support its // configuration changing. // // See also Logger.Reload, SetLevelString. type Prm struct { // support runtime rereading level zapcore.Level // SamplingHook hook for the zap.Logger SamplingHook func(e zapcore.Entry, sd zapcore.SamplingDecision) // do not support runtime rereading dest string // PrependTimestamp specifies whether to prepend a timestamp in the log PrependTimestamp bool // map of tag's bit masks to log level, overrides lvl tl map[Tag]zapcore.Level } const ( DestinationUndefined = "" DestinationStdout = "stdout" DestinationJournald = "journald" zapTagFieldName = "tag" ) // SetLevelString sets the minimum logging level. Default is // "info". // // Returns an error if s is not a string representation of a // supporting logging level. // // Supports runtime rereading. func (p *Prm) SetLevelString(s string) error { return p.level.UnmarshalText([]byte(s)) } func (p *Prm) SetDestination(d string) error { if d != DestinationStdout && d != DestinationJournald { return fmt.Errorf("invalid logger destination %s", d) } if p != nil { p.dest = d } return nil } // SetTags parses list of tags with log level. func (p *Prm) SetTags(tags [][]string) (err error) { p.tl, err = parseTags(tags) return err } // NewLogger constructs a new zap logger instance. Constructing with nil // parameters is safe: default values will be used then. // Passing non-nil parameters after a successful creation (non-error) allows // runtime reconfiguration. // // Logger is built from production logging configuration with: // - parameterized level; // - console encoding; // - ISO8601 time encoding. // // Logger records a stack trace for all messages at or above fatal level. func NewLogger(prm Prm) (*Logger, error) { switch prm.dest { case DestinationUndefined, DestinationStdout: return newConsoleLogger(prm) case DestinationJournald: return newJournaldLogger(prm) default: return nil, fmt.Errorf("unknown destination %s", prm.dest) } } func newConsoleLogger(prm Prm) (*Logger, error) { c := zap.NewProductionConfig() c.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) c.Encoding = "console" if prm.SamplingHook != nil { c.Sampling.Hook = prm.SamplingHook } if prm.PrependTimestamp { c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { c.EncoderConfig.TimeKey = "" } lZap, err := c.Build( zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)), zap.AddCallerSkip(1), ) if err != nil { return nil, err } parentZap := *lZap lZap = lZap.With(zap.String(zapTagFieldName, tagToString(TagMain))) v := atomic.Value{} v.Store(prm.tl) l := &Logger{z: lZap, pz: &parentZap, lvl: zap.NewAtomicLevelAt(prm.level), t: TagMain, tl: &v} return l, nil } func newJournaldLogger(prm Prm) (*Logger, error) { lvl := zap.NewAtomicLevelAt(zapcore.DebugLevel) c := zap.NewProductionConfig() if prm.SamplingHook != nil { c.Sampling.Hook = prm.SamplingHook } if prm.PrependTimestamp { c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { c.EncoderConfig.TimeKey = "" } encoder := zapjournald.NewPartialEncoder(zapcore.NewConsoleEncoder(c.EncoderConfig), zapjournald.SyslogFields) core := zapjournald.NewCore(lvl, encoder, &journald.Journal{}, zapjournald.SyslogFields) coreWithContext := core.With([]zapcore.Field{ zapjournald.SyslogFacility(zapjournald.LogDaemon), zapjournald.SyslogIdentifier(), zapjournald.SyslogPid(), }) var samplerOpts []zapcore.SamplerOption if c.Sampling.Hook != nil { samplerOpts = append(samplerOpts, zapcore.SamplerHook(c.Sampling.Hook)) } samplingCore := zapcore.NewSamplerWithOptions( coreWithContext, time.Second, c.Sampling.Initial, c.Sampling.Thereafter, samplerOpts..., ) lZap := zap.New(samplingCore, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)), zap.AddCallerSkip(1)) parentZap := *lZap lZap = lZap.With(zap.String(zapTagFieldName, tagToString(TagMain))) v := atomic.Value{} v.Store(prm.tl) l := &Logger{z: lZap, pz: &parentZap, lvl: zap.NewAtomicLevelAt(prm.level), t: TagMain, tl: &v} return l, nil } func (l *Logger) Reload(prm Prm) { l.lvl.SetLevel(prm.level) l.tl.Store(prm.tl) } func (l *Logger) WithOptions(options ...zap.Option) { l.z = l.z.WithOptions(options...) l.pz = l.pz.WithOptions(options...) } func (l *Logger) With(fields ...zap.Field) *Logger { c := *l c.z = l.z.With(fields...) return &c } func (l *Logger) WithTag(tag Tag) *Logger { c := *l c.t = tag c.z = c.pz.With(zap.String(zapTagFieldName, tagToString(tag))) return &c } func NewLoggerWrapper(z *zap.Logger) *Logger { tl := &atomic.Value{} tl.Store(make(map[Tag]zapcore.Level)) return &Logger{ z: z.WithOptions(zap.AddCallerSkip(1)), pz: z.WithOptions(zap.AddCallerSkip(1)), tl: tl, lvl: zap.NewAtomicLevelAt(zapcore.DebugLevel), } }