diff --git a/cmd/icingadb/main.go b/cmd/icingadb/main.go index b8503b92..a8f7e847 100644 --- a/cmd/icingadb/main.go +++ b/cmd/icingadb/main.go @@ -3,6 +3,7 @@ package main import ( "context" "github.com/icinga/icingadb/internal/command" + "github.com/icinga/icingadb/internal/logging" "github.com/icinga/icingadb/pkg/com" "github.com/icinga/icingadb/pkg/common" "github.com/icinga/icingadb/pkg/contracts" @@ -34,13 +35,23 @@ func main() { func run() int { cmd := command.New() + logs, err := logging.NewLogging( + cmd.Config.Logging.Level, + cmd.Config.Logging.Options, + ) + if err != nil { + utils.Fatal(errors.Wrap(err, "can't configure logging")) + } - logger := cmd.Logger + logger := logs.GetLogger() defer logger.Sync() logger.Info("Starting Icinga DB") - db := cmd.Database() + db, err := cmd.Database(logs.GetChildLogger("database")) + if err != nil { + logger.Fatalf("%+v", errors.Wrap(err, "can't create database connection pool from config")) + } defer db.Close() { logger.Info("Connecting to database") @@ -54,7 +65,10 @@ func run() int { logger.Fatalf("%+v", err) } - rc := cmd.Redis() + rc, err := cmd.Redis(logs.GetChildLogger("redis")) + if err != nil { + logger.Fatalf("%+v", errors.Wrap(err, "can't create Redis client from config")) + } { logger.Info("Connecting to Redis") _, err := rc.Ping(context.Background()).Result() @@ -66,9 +80,8 @@ func run() int { ctx, cancelCtx := context.WithCancel(context.Background()) defer cancelCtx() - heartbeat := icingaredis.NewHeartbeat(ctx, rc, logger) - ha := icingadb.NewHA(ctx, db, heartbeat, logger) - + heartbeat := icingaredis.NewHeartbeat(ctx, rc, logs.GetChildLogger("heartbeat")) + ha := icingadb.NewHA(ctx, db, heartbeat, logs.GetChildLogger("high-availability")) // Closing ha on exit ensures that this instance retracts its heartbeat // from the database so that another instance can take over immediately. defer func() { @@ -78,11 +91,10 @@ func run() int { ha.Close(ctx) cancelCtx() }() - - s := icingadb.NewSync(db, rc, logger) - hs := history.NewSync(db, rc, logger) - rt := icingadb.NewRuntimeUpdates(db, rc, logger) - ods := overdue.NewSync(db, rc, logger) + s := icingadb.NewSync(db, rc, logs.GetChildLogger("config-sync")) + hs := history.NewSync(db, rc, logs.GetChildLogger("history-sync")) + rt := icingadb.NewRuntimeUpdates(db, rc, logs.GetChildLogger("runtime-updates")) + ods := overdue.NewSync(db, rc, logs.GetChildLogger("overdue-sync")) sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) @@ -110,7 +122,7 @@ func run() int { logger.Fatalf("%+v", err) } - dump := icingadb.NewDumpSignals(rc, logger) + dump := icingadb.NewDumpSignals(rc, logs.GetChildLogger("dump-signals")) g.Go(func() error { logger.Info("Staring config dump signal handling") @@ -193,7 +205,7 @@ func run() int { com.ErrgroupReceive(g, dbErrs) g.Go(func() error { - return s.ApplyDelta(ctx, icingadb.NewDelta(ctx, actualCvs, cvs1, cv, logger)) + return s.ApplyDelta(ctx, icingadb.NewDelta(ctx, actualCvs, cvs1, cv, logs.GetChildLogger("config-sync"))) }) cvFlat := common.NewSyncSubject(v1.NewCustomvarFlat) @@ -208,7 +220,7 @@ func run() int { com.ErrgroupReceive(g, dbErrs) g.Go(func() error { - return s.ApplyDelta(ctx, icingadb.NewDelta(ctx, actualCvFlats, cvFlats, cvFlat, logger)) + return s.ApplyDelta(ctx, icingadb.NewDelta(ctx, actualCvFlats, cvFlats, cvFlat, logs.GetChildLogger("config-sync"))) }) return nil diff --git a/config.yml.example b/config.yml.example index 40ef95d4..28490967 100644 --- a/config.yml.example +++ b/config.yml.example @@ -1,8 +1,28 @@ +# This is the configuration file for Icinga DB. + database: host: icingadb port: 3306 database: icingadb user: icingadb password: icingadb + redis: address: redis:6380 + +logging: + # Default logging level. Can be set to 'fatal', 'error', 'warning', 'info' or 'debug'. + # If not set, defaults to 'info'. + level: + + # Map of component-logging level pairs to define a different log level than the default value for each component. + options: + database: + redis: + heartbeat: + high-availability: + config-sync: + history-sync: + runtime-updates: + overdue-sync: + dump-signals: diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md index 8b4dd11b..ca21b78a 100644 --- a/doc/03-Configuration.md +++ b/doc/03-Configuration.md @@ -24,3 +24,26 @@ port | **Required.** Database port. database | **Required.** Database database. user | **Required.** Database username. password | **Required.** Database password. + +## Logging Configuration + +Configuration of the logging component used by Icinga DB. + +Option | Description +-------------------------|----------------------------------------------- +level | **Optional.** Specifies the default logging level. Can be set to `fatal`, `error`, `warning`, `info` or `debug`. Defaults to `info`. +options | **Optional.** Map of component name to logging level in order to set a different logging level for each component instead of the default one. See [logging components](#logging-components) for details. + +### Logging Components + +Component | Description +-------------------------|----------------------------------------------- +database | Database connection status and queries. +redis | Redis connection status and queries. +heartbeat | Icinga heartbeats received through Redis. +high-availability | Manages responsibility of Icinga DB instances. +config-sync | Config object synchronization between Redis and MySQL. +history-sync | Synchronization of history entries from Redis to MySQL. +runtime-updates | Runtime updates of config objects after the initial config synchronization. +overdue-sync | Calculation and synchronization of the overdue status of checkables. +dump-signals | Dump signals received from Icinga. diff --git a/internal/command/command.go b/internal/command/command.go index 1450b445..f9c35816 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -17,7 +17,6 @@ import ( type Command struct { Flags *config.Flags Config *config.Config - Logger *zap.SugaredLogger } // New creates and returns a new Command, parses CLI flags and YAML the config, and initializes the logger. @@ -42,38 +41,18 @@ func New() *Command { utils.Fatal(err) } - loggerCfg := zap.NewDevelopmentConfig() - // Disable zap's automatic stack trace capturing, as we call errors.Wrap() before logging with "%+v". - loggerCfg.DisableStacktrace = true - logger, err := loggerCfg.Build() - if err != nil { - utils.Fatal(errors.Wrap(err, "can't create logger")) - } - sugar := logger.Sugar() - return &Command{ Flags: flags, Config: cfg, - Logger: sugar, } } // Database creates and returns a new icingadb.DB connection from config.Config. -func (c Command) Database() *icingadb.DB { - db, err := c.Config.Database.Open(c.Logger) - if err != nil { - c.Logger.Fatalf("%+v", errors.Wrap(err, "can't create database connection pool from config")) - } - - return db +func (c Command) Database(l *zap.SugaredLogger) (*icingadb.DB, error) { + return c.Config.Database.Open(l) } // Redis creates and returns a new icingaredis.Client connection from config.Config. -func (c Command) Redis() *icingaredis.Client { - rc, err := c.Config.Redis.NewClient(c.Logger) - if err != nil { - c.Logger.Fatalf("%+v", errors.Wrap(err, "can't create Redis client from config")) - } - - return rc +func (c Command) Redis(l *zap.SugaredLogger) (*icingaredis.Client, error) { + return c.Config.Redis.NewClient(l) } diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 00000000..5ea5ae53 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,107 @@ +package logging + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "sync" +) + +// 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. +type Logging struct { + level zap.AtomicLevel + logger *zap.SugaredLogger + // encoder defines the zapcore.Encoder, + // which is used to create the default logger and the child loggers + encoder zapcore.Encoder + // syncer defines the zapcore.WriterSyncer, + // which is used to create the default logger and the child loggers + syncer zapcore.WriteSyncer + mu sync.Mutex + loggers map[string]*zap.SugaredLogger + options Options +} + +// defaultEncConfig stores default zapcore.EncoderConfig for this 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 + +// NewLogging takes log level for default logger, output where log messages are written to +// and options having log levels for named child loggers and initializes a new Logging. +func NewLogging(level zapcore.Level, options Options) (*Logging, error) { + atom := zap.NewAtomicLevelAt(level) + + encoder := zapcore.NewConsoleEncoder(defaultEncConfig) + syncer := zapcore.Lock(os.Stderr) + + logger := zap.New(zapcore.NewCore( + encoder, + syncer, + atom, + )) + + return &Logging{ + level: atom, + logger: logger.Sugar(), + encoder: encoder, + syncer: syncer, + loggers: map[string]*zap.SugaredLogger{}, + 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) *zap.SugaredLogger { + l.mu.Lock() + defer l.mu.Unlock() + + if logger, ok := l.loggers[name]; ok { + return logger + } + + if level, found := l.options[name]; found { + atom := zap.NewAtomicLevelAt(level) + + logger := l.logger.Desugar().WithOptions( + zap.WrapCore(func(c zapcore.Core) zapcore.Core { + return zapcore.NewCore( + l.encoder, + l.syncer, + atom, + ) + })).Sugar().Named(name) + + l.loggers[name] = logger + + return logger + } + + logger := l.logger.Named(name) + l.loggers[name] = logger + + return logger +} + +// GetLogger returns the default logger. +func (l *Logging) GetLogger() *zap.SugaredLogger { + return l.logger +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 93d26a96..1199fce2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,6 +12,7 @@ import ( type Config struct { Database Database `yaml:"database"` Redis Redis `yaml:"redis"` + Logging Logging `yaml:"logging"` } // Validate checks constraints in the supplied configuration and returns an error if they are violated. diff --git a/pkg/config/logging.go b/pkg/config/logging.go new file mode 100644 index 00000000..205874c9 --- /dev/null +++ b/pkg/config/logging.go @@ -0,0 +1,14 @@ +package config + +import ( + "github.com/icinga/icingadb/internal/logging" + "go.uber.org/zap/zapcore" +) + +// Logging defines Logger configuration. +type Logging struct { + // zapcore.Level at 0 is for info level. + Level zapcore.Level `yaml:"level" default:"0"` + + logging.Options `yaml:"options"` +}