logger: Add journald support #971
11 changed files with 108 additions and 6 deletions
|
@ -68,6 +68,10 @@ func main() {
|
||||||
cfg.GetString("logger.level"),
|
cfg.GetString("logger.level"),
|
||||||
)
|
)
|
||||||
exitErr(err)
|
exitErr(err)
|
||||||
|
err = logPrm.SetDestination(
|
||||||
|
cfg.GetString("logger.destination"),
|
||||||
|
)
|
||||||
|
exitErr(err)
|
||||||
logPrm.SamplingHook = metrics.LogMetrics().GetSamplingHook()
|
logPrm.SamplingHook = metrics.LogMetrics().GetSamplingHook()
|
||||||
log, err = logger.NewLogger(logPrm)
|
log, err = logger.NewLogger(logPrm)
|
||||||
exitErr(err)
|
exitErr(err)
|
||||||
|
|
|
@ -96,7 +96,8 @@ type applicationConfiguration struct {
|
||||||
_read bool
|
_read bool
|
||||||
|
|
||||||
LoggerCfg struct {
|
LoggerCfg struct {
|
||||||
level string
|
level string
|
||||||
|
destination string
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineCfg struct {
|
EngineCfg struct {
|
||||||
|
@ -209,6 +210,7 @@ func (a *applicationConfiguration) readConfig(c *config.Config) error {
|
||||||
// Logger
|
// Logger
|
||||||
|
|
||||||
a.LoggerCfg.level = loggerconfig.Level(c)
|
a.LoggerCfg.level = loggerconfig.Level(c)
|
||||||
|
a.LoggerCfg.destination = loggerconfig.Destination(c)
|
||||||
|
|
||||||
// Storage Engine
|
// Storage Engine
|
||||||
|
|
||||||
|
@ -1010,6 +1012,11 @@ func (c *cfg) loggerPrm() (*logger.Prm, error) {
|
||||||
// not expected since validation should be performed before
|
// not expected since validation should be performed before
|
||||||
panic(fmt.Sprintf("incorrect log level format: %s", c.LoggerCfg.level))
|
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
|
||||||
fyrchik marked this conversation as resolved
|
|||||||
|
panic(fmt.Sprintf("incorrect log destination format: %s", c.LoggerCfg.destination))
|
||||||
|
}
|
||||||
|
|
||||||
return c.dynamicConfiguration.logger, nil
|
return c.dynamicConfiguration.logger, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config"
|
"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"
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/logging/lokicore/loki"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// LevelDefault is a default logger level.
|
// LevelDefault is a default logger level.
|
||||||
LevelDefault = "info"
|
LevelDefault = "info"
|
||||||
|
DestinationDefault = logger.DestinationStdout
|
||||||
subsection = "logger"
|
subsection = "logger"
|
||||||
lokiSubsection = "loki"
|
lokiSubsection = "loki"
|
||||||
AddressDefault = "localhost:3100"
|
AddressDefault = "localhost:3100"
|
||||||
|
@ -34,6 +36,22 @@ func Level(c *config.Config) string {
|
||||||
return LevelDefault
|
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.
|
// ToLokiConfig extracts loki config.
|
||||||
func ToLokiConfig(c *config.Config) loki.Config {
|
func ToLokiConfig(c *config.Config) loki.Config {
|
||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
|
|
|
@ -11,15 +11,15 @@ import (
|
||||||
|
|
||||||
func TestLoggerSection_Level(t *testing.T) {
|
func TestLoggerSection_Level(t *testing.T) {
|
||||||
t.Run("defaults", func(t *testing.T) {
|
t.Run("defaults", func(t *testing.T) {
|
||||||
v := loggerconfig.Level(configtest.EmptyConfig())
|
require.Equal(t, loggerconfig.LevelDefault, loggerconfig.Level(configtest.EmptyConfig()))
|
||||||
require.Equal(t, loggerconfig.LevelDefault, v)
|
require.Equal(t, loggerconfig.DestinationDefault, loggerconfig.Destination(configtest.EmptyConfig()))
|
||||||
})
|
})
|
||||||
|
|
||||||
const path = "../../../../config/example/node"
|
const path = "../../../../config/example/node"
|
||||||
|
|
||||||
fileConfigTest := func(c *config.Config) {
|
fileConfigTest := func(c *config.Config) {
|
||||||
v := loggerconfig.Level(c)
|
require.Equal(t, "debug", loggerconfig.Level(c))
|
||||||
require.Equal(t, "debug", v)
|
require.Equal(t, "journald", loggerconfig.Destination(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
configtest.ForEachFileType(path, fileConfigTest)
|
configtest.ForEachFileType(path, fileConfigTest)
|
||||||
|
|
|
@ -25,6 +25,11 @@ func validateConfig(c *config.Config) error {
|
||||||
return fmt.Errorf("invalid logger level: %w", err)
|
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
|
// shard configuration validation
|
||||||
|
|
||||||
shardNum := 0
|
shardNum := 0
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
FROSTFS_LOGGER_LEVEL=debug
|
FROSTFS_LOGGER_LEVEL=debug
|
||||||
|
FROSTFS_LOGGER_DESTINATION=journald
|
||||||
|
|
||||||
FROSTFS_PPROF_ENABLED=true
|
FROSTFS_PPROF_ENABLED=true
|
||||||
FROSTFS_PPROF_ADDRESS=localhost:6060
|
FROSTFS_PPROF_ADDRESS=localhost:6060
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"logger": {
|
"logger": {
|
||||||
"level": "debug"
|
"level": "debug",
|
||||||
|
"destination": "journald"
|
||||||
},
|
},
|
||||||
"pprof": {
|
"pprof": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
logger:
|
logger:
|
||||||
level: debug # logger level: one of "debug", "info" (default), "warn", "error", "dpanic", "panic", "fatal"
|
level: debug # logger level: one of "debug", "info" (default), "warn", "error", "dpanic", "panic", "fatal"
|
||||||
|
destination: journald # logger destination: one of "stdout" (default), "journald"
|
||||||
|
|
||||||
systemdnotify:
|
systemdnotify:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -10,6 +10,7 @@ require (
|
||||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
||||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240129064140-8d21ab2d99d9
|
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240129064140-8d21ab2d99d9
|
||||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
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/cheggaaa/pb v1.0.29
|
||||||
github.com/chzyer/readline v1.5.1
|
github.com/chzyer/readline v1.5.1
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
|
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/cobra v1.8.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
github.com/ssgreg/journald v1.0.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.8
|
||||||
go.opentelemetry.io/otel v1.22.0
|
go.opentelemetry.io/otel v1.22.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -16,6 +16,8 @@ git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9m
|
||||||
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc=
|
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc=
|
||||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA=
|
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA=
|
||||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0/go.mod h1:dhY+oy274hV8wGvGL4MwwMpdL3GYvaX1a8GQZQHvlF8=
|
git.frostfs.info/TrueCloudLab/tzhash v1.8.0/go.mod h1:dhY+oy274hV8wGvGL4MwwMpdL3GYvaX1a8GQZQHvlF8=
|
||||||
|
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02 h1:HeY8n27VyPRQe49l/fzyVMkWEB2fsLJYKp64pwA7tz4=
|
||||||
|
git.frostfs.info/TrueCloudLab/zapjournald v0.0.0-20240124114243-cb2e66427d02/go.mod h1:rQFJJdEOV7KbbMtQYR2lNfiZk+ONRDJSbMCTWxKt8Fw=
|
||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
|
@ -239,6 +241,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||||
|
github.com/ssgreg/journald v1.0.0 h1:0YmTDPJXxcWDPba12qNMdO6TxvfkFSYpFIJ31CwmLcU=
|
||||||
|
github.com/ssgreg/journald v1.0.0/go.mod h1:RUckwmTM8ghGWPslq2+ZBZzbb9/2KgjzYZ4JEP+oRt0=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/zapjournald"
|
||||||
|
"github.com/ssgreg/journald"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
@ -35,8 +39,15 @@ type Prm struct {
|
||||||
SamplingHook func(e zapcore.Entry, sd zapcore.SamplingDecision)
|
SamplingHook func(e zapcore.Entry, sd zapcore.SamplingDecision)
|
||||||
|
|
||||||
// do not support runtime rereading
|
// do not support runtime rereading
|
||||||
|
dest string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DestinationUndefined = ""
|
||||||
fyrchik
commented
We also have We also have `Loki` which doesn't use this field. What about unifying the approach? (`destination: loki`)
dstepanov-yadro
commented
`loki` works with both of `journald` and `stdout`, it is not a destination.
fyrchik
commented
Journald core can work with stdout too (it does by default I think) Journald core can work with stdout too (it does by default I think)
dstepanov-yadro
commented
So then So then `destination` possible values will be `stdout`, `journald`, `stdout+loki`, `journald+loki`? Looks ... strange.
|
|||||||
|
DestinationStdout = "stdout"
|
||||||
|
DestinationJournald = "journald"
|
||||||
|
)
|
||||||
|
|
||||||
// SetLevelString sets the minimum logging level. Default is
|
// SetLevelString sets the minimum logging level. Default is
|
||||||
// "info".
|
// "info".
|
||||||
//
|
//
|
||||||
|
@ -48,6 +59,16 @@ func (p *Prm) SetLevelString(s string) error {
|
||||||
return p.level.UnmarshalText([]byte(s))
|
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.
|
// Reload reloads configuration of a connected instance of the Logger.
|
||||||
// Returns ErrLoggerNotConnected if no connection has been performed.
|
// Returns ErrLoggerNotConnected if no connection has been performed.
|
||||||
// Returns any reconfiguration error from the Logger directly.
|
// Returns any reconfiguration error from the Logger directly.
|
||||||
|
@ -79,7 +100,17 @@ func NewLogger(prm *Prm) (*Logger, error) {
|
||||||
if prm == nil {
|
if prm == nil {
|
||||||
prm = defaultPrm()
|
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)
|
lvl := zap.NewAtomicLevelAt(prm.level)
|
||||||
|
|
||||||
c := zap.NewProductionConfig()
|
c := zap.NewProductionConfig()
|
||||||
|
@ -103,6 +134,34 @@ func NewLogger(prm *Prm) (*Logger, error) {
|
||||||
return l, nil
|
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 {
|
func (l *Logger) reload(prm Prm) error {
|
||||||
l.lvl.SetLevel(prm.level)
|
l.lvl.SetLevel(prm.level)
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Add table
Reference in a new issue
Where is validation performed?
I see a comment in the previous
if
but it could be misleading too.Also, do we change this on SIGHUP? Not saying that we need to (it could be even harmful), just to know
Here:
cmd/frostfs-node/validate.go
No.