diff --git a/internal/configmgr/configmgr.go b/internal/configmgr/configmgr.go new file mode 100644 index 00000000000..630a28dd8c6 --- /dev/null +++ b/internal/configmgr/configmgr.go @@ -0,0 +1,39 @@ +// Package configmgr defines AdGuard Home on-disk configuration entities. +package configmgr + +import ( + "github.com/AdguardTeam/golibs/container" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/validate" +) + +// Config is the top-level on-disk configuration structure. +// +// TODO(d.kolyshev): Use. +type Config struct { + // Log is a block with log configuration settings. + Log *LogConfig `yaml:"log"` +} + +// type check +var _ validate.Interface = (*Config)(nil) + +// Validate implements the [validate.Interface] interface for *Config. +func (c *Config) Validate() (err error) { + if c == nil { + return errors.ErrNoValue + } + + // Keep this in the same order as the fields in the config. + validators := container.KeyValues[string, validate.Interface]{{ + Key: "log", + Value: c.Log, + }} + + var errs []error + for _, kv := range validators { + errs = validate.Append(errs, kv.Key, kv.Value) + } + + return errors.Join(errs...) +} diff --git a/internal/configmgr/log.go b/internal/configmgr/log.go new file mode 100644 index 00000000000..a2ce80bc5a7 --- /dev/null +++ b/internal/configmgr/log.go @@ -0,0 +1,53 @@ +package configmgr + +import ( + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/validate" +) + +// LogConfig is the on-disk logging configuration. +type LogConfig struct { + // File is the path to the log file. If empty, logs are written to stdout. + // If "syslog", logs are written to syslog. + File string `yaml:"file"` + + // MaxAge is the maximum duration for retaining old log files, in days. + MaxAge int `yaml:"max_age"` + + // MaxBackups is the maximum number of old log files to retain. + // + // NOTE: MaxAge may still cause them to get deleted. + MaxBackups int `yaml:"max_backups"` + + // MaxSize is the maximum size of the log file before it gets rotated, in + // megabytes. + MaxSize int `yaml:"max_size"` + + // Compress determines, if the rotated log files should be compressed using + // gzip. + Compress bool `yaml:"compress"` + + // Enabled indicates whether logging is enabled. + Enabled bool `yaml:"enabled"` + + // LocalTime determines, if the time used for formatting the timestamps in + // is the computer's local time. + LocalTime bool `yaml:"local_time"` + + // Verbose determines, if verbose (aka debug) logging is enabled. + Verbose bool `yaml:"verbose"` +} + +// type check +var _ validate.Interface = (*LogConfig)(nil) + +// Validate implements the [validate.Interface] interface for *LogConfig. +// +// TODO(d.kolyshev): Add more validations. +func (c *LogConfig) Validate() (err error) { + if c == nil { + return errors.ErrNoValue + } + + return nil +} diff --git a/internal/home/config.go b/internal/home/config.go index 036bb0d86ce..ecb4f8b4c0c 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -15,6 +15,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghtls" + "github.com/AdguardTeam/AdGuardHome/internal/configmgr" "github.com/AdguardTeam/AdGuardHome/internal/configmigrate" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" @@ -42,39 +43,6 @@ const ( userFilterDataDir = "userfilters" ) -// logSettings are the logging settings part of the configuration file. -type logSettings struct { - // Enabled indicates whether logging is enabled. - Enabled bool `yaml:"enabled"` - - // File is the path to the log file. If empty, logs are written to stdout. - // If "syslog", logs are written to syslog. - File string `yaml:"file"` - - // MaxBackups is the maximum number of old log files to retain. - // - // NOTE: MaxAge may still cause them to get deleted. - MaxBackups int `yaml:"max_backups"` - - // MaxSize is the maximum size of the log file before it gets rotated, in - // megabytes. The default value is 100 MB. - MaxSize int `yaml:"max_size"` - - // MaxAge is the maximum duration for retaining old log files, in days. - MaxAge int `yaml:"max_age"` - - // Compress determines, if the rotated log files should be compressed using - // gzip. - Compress bool `yaml:"compress"` - - // LocalTime determines, if the time used for formatting the timestamps in - // is the computer's local time. - LocalTime bool `yaml:"local_time"` - - // Verbose determines, if verbose (aka debug) logging is enabled. - Verbose bool `yaml:"verbose"` -} - // osConfig contains OS-related configuration. type osConfig struct { // Group is the name of the group which AdGuard Home must switch to on @@ -109,6 +77,8 @@ type clientSourcesConfig struct { // // Field ordering is important, YAML fields better not to be reordered, if it's // not absolutely necessary. +// +// TODO(d.kolyshev): Use [configmgr.Config]. type configuration struct { // Raw file data to avoid re-reading of configuration file // It's reset after config is parsed @@ -158,7 +128,7 @@ type configuration struct { Clients *clientsConfig `yaml:"clients"` // Log is a block with log configuration settings. - Log logSettings `yaml:"log"` + Log *configmgr.LogConfig `yaml:"log"` OSConfig *osConfig `yaml:"os"` @@ -576,16 +546,6 @@ var config = &configuration{ HostsFile: true, }, }, - Log: logSettings{ - Enabled: true, - File: "", - MaxBackups: 0, - MaxSize: 100, - MaxAge: 3, - Compress: false, - LocalTime: false, - Verbose: false, - }, OSConfig: &osConfig{}, SchemaVersion: configmigrate.LastSchemaVersion, Theme: ThemeAuto, diff --git a/internal/home/home.go b/internal/home/home.go index a740d04bbcf..393b64feb56 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -96,7 +96,7 @@ func Main(clientBuildFS fs.FS) { confPath := initConfigFilename(ctx, l, opts, workDir) - ls := getLogSettings(ctx, l, opts, workDir, confPath) + ls := newLogSettings(ctx, l, opts, workDir, confPath) // TODO(a.garipov): Use slog everywhere. baseLogger := newSlogLogger(ls) diff --git a/internal/home/log.go b/internal/home/log.go index 2c3a648d284..36cda531257 100644 --- a/internal/home/log.go +++ b/internal/home/log.go @@ -9,6 +9,7 @@ import ( "runtime" "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/AdGuardHome/internal/configmgr" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil" yaml "go.yaml.in/yaml/v4" @@ -19,32 +20,58 @@ import ( // for logger output. const configSyslog = "syslog" +// logSettings are the logging settings part of the configuration file. +type logSettings struct { + // file is the path to the log file. If empty, logs are written to stdout. + // If "syslog", logs are written to syslog. + file string + + // maxAge is the maximum duration for retaining old log files, in days. + maxAge int + + // maxBackups is the maximum number of old log files to retain. + // + // NOTE: maxAge may still cause them to get deleted. + maxBackups int + + // maxSize is the maximum size of the log file before it gets rotated, in + // megabytes. The default value is 100 MB. + maxSize int + + // compress determines, if the rotated log files should be compressed using + // gzip. + compress bool + + // enabled indicates whether logging is enabled. + enabled bool + + // localTime determines, if the time used for formatting the timestamps in + // is the computer's local time. + localTime bool + + // verbose determines, if verbose (aka debug) logging is enabled. + verbose bool +} + // newSlogLogger returns new [*slog.Logger] configured with the given settings. // ls must not be nil. func newSlogLogger(ls *logSettings) (l *slog.Logger) { - if !ls.Enabled { + if !ls.enabled { return slogutil.NewDiscardLogger() } lvl := slog.LevelInfo - if ls.Verbose { + if ls.verbose { lvl = slog.LevelDebug + + log.SetLevel(log.DEBUG) } - l = slogutil.New(&slogutil.Config{ + return slogutil.New(&slogutil.Config{ Format: slogutil.FormatAdGuardLegacy, Level: lvl, AddTimestamp: true, }) - - // Configure logger level. - if !ls.Enabled { - log.SetLevel(log.OFF) - } else if ls.Verbose { - log.SetLevel(log.DEBUG) - } - - return l } // configureLogger configures logger output. ls must not be nil. @@ -54,11 +81,11 @@ func configureLogger(ls *logSettings, workDir string) (err error) { log.SetFlags(log.LstdFlags | log.Lmicroseconds) // Write logs to stdout by default. - if ls.File == "" { + if ls.file == "" { return nil } - if ls.File == configSyslog { + if ls.file == configSyslog { // Use syslog where it is possible and eventlog on Windows. err = aghos.ConfigureSyslog(serviceName) if err != nil { @@ -68,51 +95,61 @@ func configureLogger(ls *logSettings, workDir string) (err error) { return nil } - logFilePath := ls.File + logFilePath := ls.file if !filepath.IsAbs(logFilePath) { logFilePath = filepath.Join(workDir, logFilePath) } log.SetOutput(&lumberjack.Logger{ Filename: logFilePath, - Compress: ls.Compress, - LocalTime: ls.LocalTime, - MaxBackups: ls.MaxBackups, - MaxSize: ls.MaxSize, - MaxAge: ls.MaxAge, + Compress: ls.compress, + LocalTime: ls.localTime, + MaxBackups: ls.maxBackups, + MaxSize: ls.maxSize, + MaxAge: ls.maxAge, }) return err } -// getLogSettings returns a log settings object properly initialized from opts. -// l must not be nil. -func getLogSettings( +// Default log constants. +const ( + defaultLogMaxAge = 3 + defaultLogMaxSize = 100 +) + +// newLogSettings returns a *logSettings properly initialized from opts. l must +// not be nil. +func newLogSettings( ctx context.Context, l *slog.Logger, opts options, workDir string, confPath string, ) (ls *logSettings) { - configLogSettings := config.Log - ls = readLogSettings(ctx, l, workDir, confPath) if ls == nil { // Use default log settings. - ls = &configLogSettings + ls = &logSettings{ + enabled: true, + maxAge: defaultLogMaxAge, + maxSize: defaultLogMaxSize, + } } + config.Log = ls.toLogConf() + // Command-line arguments can override config settings. if opts.verbose { - ls.Verbose = true + ls.verbose = true } - ls.File = cmp.Or(opts.logFile, ls.File) + ls.file = cmp.Or(opts.logFile, ls.file) - if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" { + if opts.runningAsService && ls.file == "" && runtime.GOOS == "windows" { // When running as a Windows service, use eventlog by default if // nothing else is configured. Otherwise, we'll lose the log output. - ls.File = configSyslog + ls.file = configSyslog } return ls @@ -127,24 +164,59 @@ func readLogSettings( workDir string, confPath string, ) (ls *logSettings) { - // TODO(s.chzhen): Add a helper function that returns default parameters - // for this structure and for the global configuration structure [config]. - conf := &configuration{ - Log: logSettings{ - // By default, it is true if the property does not exist. - Enabled: true, - }, - } - yamlFile, err := readConfigFile(ctx, l, workDir, confPath) if err != nil { + l.DebugContext(ctx, "reading config file", slogutil.KeyError, err) + return nil } + conf := &configuration{} err = yaml.Unmarshal(yamlFile, conf) if err != nil { l.ErrorContext(ctx, "getting logging settings from config", slogutil.KeyError, err) } - return &conf.Log + err = conf.Log.Validate() + if err != nil { + l.ErrorContext(ctx, "reading logging settings from config", slogutil.KeyError, err) + + return nil + } + + return logConfToInternal(conf.Log) +} + +// logConfToInternal converts c to the log settings. c must be valid. +func logConfToInternal(c *configmgr.LogConfig) (s *logSettings) { + if c == nil { + return &logSettings{ + enabled: true, + } + } + + return &logSettings{ + enabled: c.Enabled, + file: c.File, + maxAge: c.MaxAge, + maxBackups: c.MaxBackups, + maxSize: c.MaxSize, + compress: c.Compress, + localTime: c.LocalTime, + verbose: c.Verbose, + } +} + +// toLogConf converts s to the on-disk logging configuration. s must be valid. +func (s *logSettings) toLogConf() (c *configmgr.LogConfig) { + return &configmgr.LogConfig{ + File: s.file, + MaxAge: s.maxAge, + MaxBackups: s.maxBackups, + MaxSize: s.maxSize, + Compress: s.compress, + Enabled: s.enabled, + LocalTime: s.localTime, + Verbose: s.verbose, + } } diff --git a/scripts/make/go-lint.sh b/scripts/make/go-lint.sh index 908adb33c51..c4c12da302d 100644 --- a/scripts/make/go-lint.sh +++ b/scripts/make/go-lint.sh @@ -268,6 +268,7 @@ run_linter "$go" tool fieldalignment \ ./internal/aghuser/ \ ./internal/arpdb/ \ ./internal/client/ \ + ./internal/configmgr/ \ ./internal/configmigrate/ \ ./internal/dhcpsvc/ \ ./internal/filtering/hashprefix/ \