diff options
Diffstat (limited to 'pkg/logging')
-rw-r--r-- | pkg/logging/journald_core.go | 85 | ||||
-rw-r--r-- | pkg/logging/logger.go | 26 | ||||
-rw-r--r-- | pkg/logging/logging.go | 131 |
3 files changed, 242 insertions, 0 deletions
diff --git a/pkg/logging/journald_core.go b/pkg/logging/journald_core.go new file mode 100644 index 0000000..50dd39f --- /dev/null +++ b/pkg/logging/journald_core.go @@ -0,0 +1,85 @@ +package logging + +import ( + "github.com/icinga/icingadb/pkg/utils" + "github.com/pkg/errors" + "github.com/ssgreg/journald" + "go.uber.org/zap/zapcore" + "strings" + "unicode" +) + +// priorities maps zapcore.Level to journal.Priority. +var priorities = map[zapcore.Level]journald.Priority{ + zapcore.DebugLevel: journald.PriorityDebug, + zapcore.InfoLevel: journald.PriorityInfo, + zapcore.WarnLevel: journald.PriorityWarning, + zapcore.ErrorLevel: journald.PriorityErr, + zapcore.FatalLevel: journald.PriorityCrit, + zapcore.PanicLevel: journald.PriorityCrit, + zapcore.DPanicLevel: journald.PriorityCrit, +} + +// NewJournaldCore returns a zapcore.Core that sends log entries to systemd-journald and +// uses the given identifier as a prefix for structured logging context that is sent as journal fields. +func NewJournaldCore(identifier string, enab zapcore.LevelEnabler) zapcore.Core { + return &journaldCore{ + LevelEnabler: enab, + identifier: identifier, + identifierU: strings.ToUpper(identifier), + } +} + +type journaldCore struct { + zapcore.LevelEnabler + context []zapcore.Field + identifier string + identifierU string +} + +func (c *journaldCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(ent.Level) { + return ce.AddCore(ent, c) + } + + return ce +} + +func (c *journaldCore) Sync() error { + return nil +} + +func (c *journaldCore) With(fields []zapcore.Field) zapcore.Core { + cc := *c + cc.context = append(cc.context[:len(cc.context):len(cc.context)], fields...) + + return &cc +} + +func (c *journaldCore) Write(ent zapcore.Entry, fields []zapcore.Field) error { + pri, ok := priorities[ent.Level] + if !ok { + return errors.Errorf("unknown log level %q", ent.Level) + } + + enc := zapcore.NewMapObjectEncoder() + c.addFields(enc, fields) + c.addFields(enc, c.context) + enc.Fields["SYSLOG_IDENTIFIER"] = c.identifier + + message := ent.Message + if ent.LoggerName != c.identifier { + message = ent.LoggerName + ": " + message + } + + return journald.Send(message, pri, enc.Fields) +} + +func (c *journaldCore) addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) { + for _, field := range fields { + field.Key = c.identifierU + + "_" + + utils.ConvertCamelCase(field.Key, unicode.UpperCase, '_') + field.AddTo(enc) + } +} diff --git a/pkg/logging/logger.go b/pkg/logging/logger.go new file mode 100644 index 0000000..490445e --- /dev/null +++ b/pkg/logging/logger.go @@ -0,0 +1,26 @@ +package logging + +import ( + "go.uber.org/zap" + "time" +) + +// Logger wraps zap.SugaredLogger and +// allows to get the interval for periodic logging. +type Logger struct { + *zap.SugaredLogger + interval time.Duration +} + +// NewLogger returns a new Logger. +func NewLogger(base *zap.SugaredLogger, interval time.Duration) *Logger { + return &Logger{ + SugaredLogger: base, + interval: interval, + } +} + +// Interval returns the interval for periodic logging. +func (l *Logger) Interval() time.Duration { + return l.interval +} diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go new file mode 100644 index 0000000..e310695 --- /dev/null +++ b/pkg/logging/logging.go @@ -0,0 +1,131 @@ +package logging + +import ( + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "sync" + "time" +) + +const ( + CONSOLE = "console" + JOURNAL = "systemd-journald" +) + +// defaultEncConfig defines the default zapcore.EncoderConfig for the logging package. +var defaultEncConfig = zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, +} + +// Options define child loggers with their desired log level. +type Options map[string]zapcore.Level + +// Logging implements access to a default logger and named child loggers. +// Log levels can be configured per named child via Options which, if not configured, +// fall back on a default log level. +// Logs either to the console or to systemd-journald. +type Logging struct { + logger *Logger + output string + verbosity zap.AtomicLevel + interval time.Duration + + // coreFactory creates zapcore.Core based on the log level and the log output. + coreFactory func(zap.AtomicLevel) zapcore.Core + + mu sync.Mutex + loggers map[string]*Logger + + options Options +} + +// NewLogging takes the name and log level for the default logger, +// output where log messages are written to, +// options having log levels for named child loggers +// and returns a new Logging. +func NewLogging(name string, level zapcore.Level, output string, options Options, interval time.Duration) (*Logging, error) { + verbosity := zap.NewAtomicLevelAt(level) + + var coreFactory func(zap.AtomicLevel) zapcore.Core + switch output { + case CONSOLE: + enc := zapcore.NewConsoleEncoder(defaultEncConfig) + ws := zapcore.Lock(os.Stderr) + coreFactory = func(verbosity zap.AtomicLevel) zapcore.Core { + return zapcore.NewCore(enc, ws, verbosity) + } + case JOURNAL: + coreFactory = func(verbosity zap.AtomicLevel) zapcore.Core { + return NewJournaldCore(name, verbosity) + } + default: + return nil, invalidOutput(output) + } + + logger := NewLogger(zap.New(coreFactory(verbosity)).Named(name).Sugar(), interval) + + return &Logging{ + logger: logger, + output: output, + verbosity: verbosity, + interval: interval, + coreFactory: coreFactory, + loggers: make(map[string]*Logger), + options: options, + }, + nil +} + +// GetChildLogger returns a named child logger. +// Log levels for named child loggers are obtained from the logging options and, if not found, +// set to the default log level. +func (l *Logging) GetChildLogger(name string) *Logger { + l.mu.Lock() + defer l.mu.Unlock() + + if logger, ok := l.loggers[name]; ok { + return logger + } + + var verbosity zap.AtomicLevel + if level, found := l.options[name]; found { + verbosity = zap.NewAtomicLevelAt(level) + } else { + verbosity = l.verbosity + } + + logger := NewLogger(zap.New(l.coreFactory(verbosity)).Named(name).Sugar(), l.interval) + l.loggers[name] = logger + + return logger +} + +// GetLogger returns the default logger. +func (l *Logging) GetLogger() *Logger { + return l.logger +} + +// AssertOutput returns an error if output is not a valid logger output. +func AssertOutput(o string) error { + if o == CONSOLE || o == JOURNAL { + return nil + } + + return invalidOutput(o) +} + +func invalidOutput(o string) error { + return fmt.Errorf("%s is not a valid logger output. Must be either %q or %q", o, CONSOLE, JOURNAL) +} |