summaryrefslogtreecommitdiffstats
path: root/pkg/logging
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/logging')
-rw-r--r--pkg/logging/journald_core.go85
-rw-r--r--pkg/logging/logger.go26
-rw-r--r--pkg/logging/logging.go131
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)
+}