diff --git a/cmd/frostfs-ir/main.go b/cmd/frostfs-ir/main.go index 2b39f78e3..9879342b7 100644 --- a/cmd/frostfs-ir/main.go +++ b/cmd/frostfs-ir/main.go @@ -68,6 +68,10 @@ func main() { cfg.GetString("logger.level"), ) exitErr(err) + err = logPrm.SetDestination( + cfg.GetString("logger.destination"), + ) + exitErr(err) logPrm.SamplingHook = metrics.LogMetrics().GetSamplingHook() log, err = logger.NewLogger(logPrm) exitErr(err) diff --git a/cmd/frostfs-node/config.go b/cmd/frostfs-node/config.go index 29582c782..8b55d9c19 100644 --- a/cmd/frostfs-node/config.go +++ b/cmd/frostfs-node/config.go @@ -96,7 +96,8 @@ type applicationConfiguration struct { _read bool LoggerCfg struct { - level string + level string + destination string } EngineCfg struct { @@ -209,6 +210,7 @@ func (a *applicationConfiguration) readConfig(c *config.Config) error { // Logger a.LoggerCfg.level = loggerconfig.Level(c) + a.LoggerCfg.destination = loggerconfig.Destination(c) // Storage Engine @@ -1010,6 +1012,11 @@ func (c *cfg) loggerPrm() (*logger.Prm, error) { // not expected since validation should be performed before panic(fmt.Sprintf("incorrect log level format: %s", c.LoggerCfg.level)) } + err = c.dynamicConfiguration.logger.SetDestination(c.LoggerCfg.destination) + if err != nil { + // not expected since validation should be performed before + panic(fmt.Sprintf("incorrect log destination format: %s", c.LoggerCfg.destination)) + } return c.dynamicConfiguration.logger, nil } diff --git a/cmd/frostfs-node/config/logger/config.go b/cmd/frostfs-node/config/logger/config.go index 02645c543..378b9d793 100644 --- a/cmd/frostfs-node/config/logger/config.go +++ b/cmd/frostfs-node/config/logger/config.go @@ -5,12 +5,14 @@ import ( "time" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config" + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger" "git.frostfs.info/TrueCloudLab/frostfs-observability/logging/lokicore/loki" ) const ( // LevelDefault is a default logger level. LevelDefault = "info" + DestinationDefault = logger.DestinationStdout subsection = "logger" lokiSubsection = "loki" AddressDefault = "localhost:3100" @@ -34,6 +36,22 @@ func Level(c *config.Config) string { return LevelDefault } +// Destination returns the value of "destination" config parameter +// from "logger" section. +// +// Returns DestinationDefault if the value is not a non-empty string. +func Destination(c *config.Config) string { + v := config.StringSafe( + c.Sub(subsection), + "destination", + ) + if v != "" { + return v + } + + return DestinationDefault +} + // ToLokiConfig extracts loki config. func ToLokiConfig(c *config.Config) loki.Config { hostname, _ := os.Hostname() diff --git a/cmd/frostfs-node/config/logger/config_test.go b/cmd/frostfs-node/config/logger/config_test.go index 8a0acbd85..3587a0ddb 100644 --- a/cmd/frostfs-node/config/logger/config_test.go +++ b/cmd/frostfs-node/config/logger/config_test.go @@ -11,15 +11,15 @@ import ( func TestLoggerSection_Level(t *testing.T) { t.Run("defaults", func(t *testing.T) { - v := loggerconfig.Level(configtest.EmptyConfig()) - require.Equal(t, loggerconfig.LevelDefault, v) + require.Equal(t, loggerconfig.LevelDefault, loggerconfig.Level(configtest.EmptyConfig())) + require.Equal(t, loggerconfig.DestinationDefault, loggerconfig.Destination(configtest.EmptyConfig())) }) const path = "../../../../config/example/node" fileConfigTest := func(c *config.Config) { - v := loggerconfig.Level(c) - require.Equal(t, "debug", v) + require.Equal(t, "debug", loggerconfig.Level(c)) + require.Equal(t, "journald", loggerconfig.Destination(c)) } configtest.ForEachFileType(path, fileConfigTest) diff --git a/cmd/frostfs-node/validate.go b/cmd/frostfs-node/validate.go index 80c90ec44..ae52b9e4a 100644 --- a/cmd/frostfs-node/validate.go +++ b/cmd/frostfs-node/validate.go @@ -25,6 +25,11 @@ func validateConfig(c *config.Config) error { return fmt.Errorf("invalid logger level: %w", err) } + err = loggerPrm.SetDestination(loggerconfig.Destination(c)) + if err != nil { + return fmt.Errorf("invalid logger destination: %w", err) + } + // shard configuration validation shardNum := 0 diff --git a/config/example/node.env b/config/example/node.env index 2972f56e9..a1db0c876 100644 --- a/config/example/node.env +++ b/config/example/node.env @@ -1,4 +1,5 @@ FROSTFS_LOGGER_LEVEL=debug +FROSTFS_LOGGER_DESTINATION=journald FROSTFS_PPROF_ENABLED=true FROSTFS_PPROF_ADDRESS=localhost:6060 diff --git a/config/example/node.json b/config/example/node.json index 8dda5e606..a7d7a3651 100644 --- a/config/example/node.json +++ b/config/example/node.json @@ -1,6 +1,7 @@ { "logger": { - "level": "debug" + "level": "debug", + "destination": "journald" }, "pprof": { "enabled": true, diff --git a/config/example/node.yaml b/config/example/node.yaml index 171515164..678ee1a87 100644 --- a/config/example/node.yaml +++ b/config/example/node.yaml @@ -1,5 +1,6 @@ logger: level: debug # logger level: one of "debug", "info" (default), "warn", "error", "dpanic", "panic", "fatal" + destination: journald # logger destination: one of "stdout" (default), "journald" systemdnotify: enabled: true diff --git a/go.mod b/go.mod index 07097c547..aa8ea4ff1 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( git.frostfs.info/TrueCloudLab/hrw v1.2.1 git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240129064140-8d21ab2d99d9 git.frostfs.info/TrueCloudLab/tzhash v1.8.0 + git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02 github.com/cheggaaa/pb v1.0.29 github.com/chzyer/readline v1.5.1 github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 @@ -29,6 +30,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 + github.com/ssgreg/journald v1.0.0 github.com/stretchr/testify v1.8.4 go.etcd.io/bbolt v1.3.8 go.opentelemetry.io/otel v1.22.0 diff --git a/go.sum b/go.sum index 5243ac435..995487299 100644 Binary files a/go.sum and b/go.sum differ diff --git a/pkg/util/logger/logger.go b/pkg/util/logger/logger.go index f9a5fe38f..c6db04df9 100644 --- a/pkg/util/logger/logger.go +++ b/pkg/util/logger/logger.go @@ -1,6 +1,10 @@ package logger import ( + "fmt" + + "git.frostfs.info/TrueCloudLab/zapjournald" + "github.com/ssgreg/journald" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -35,8 +39,15 @@ type Prm struct { SamplingHook func(e zapcore.Entry, sd zapcore.SamplingDecision) // do not support runtime rereading + dest string } +const ( + DestinationUndefined = "" + DestinationStdout = "stdout" + DestinationJournald = "journald" +) + // SetLevelString sets the minimum logging level. Default is // "info". // @@ -48,6 +59,16 @@ 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 +} + // Reload reloads configuration of a connected instance of the Logger. // Returns ErrLoggerNotConnected if no connection has been performed. // Returns any reconfiguration error from the Logger directly. @@ -79,7 +100,17 @@ func NewLogger(prm *Prm) (*Logger, error) { if prm == nil { prm = defaultPrm() } + 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) { lvl := zap.NewAtomicLevelAt(prm.level) c := zap.NewProductionConfig() @@ -103,6 +134,34 @@ func NewLogger(prm *Prm) (*Logger, error) { return l, nil } +func newJournaldLogger(prm *Prm) (*Logger, error) { + lvl := zap.NewAtomicLevelAt(prm.level) + + c := zap.NewProductionConfig() + c.Level = lvl + c.Encoding = "console" + c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + if prm.SamplingHook != nil { + c.Sampling.Hook = prm.SamplingHook + } + + encoder := zapjournald.NewPartialEncoder(zapcore.NewConsoleEncoder(c.EncoderConfig), zapjournald.SyslogFields) + + core := zapjournald.NewCore(zap.NewAtomicLevelAt(prm.level), encoder, &journald.Journal{}, zapjournald.SyslogFields) + coreWithContext := core.With([]zapcore.Field{ + zapjournald.SyslogFacility(zapjournald.LogDaemon), + zapjournald.SyslogIdentifier(), + zapjournald.SyslogPid(), + }) + + lZap := zap.New(coreWithContext, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel))) + + l := &Logger{Logger: lZap, lvl: lvl} + prm._log = l + + return l, nil +} + func (l *Logger) reload(prm Prm) error { l.lvl.SetLevel(prm.level) return nil