diff --git a/CHANGELOG-3.5.md b/CHANGELOG-3.5.md index 9dedc977bb9..bb0a5b591ae 100644 --- a/CHANGELOG-3.5.md +++ b/CHANGELOG-3.5.md @@ -161,6 +161,8 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change. - Setting this flag enables `SO_REUSEADDR` which allows binding to an address in `TIME_WAIT` state, improving etcd restart time. - Reduce [around 30% memory allocation by logging range response size without marshal](https://github.com/etcd-io/etcd/pull/12871). - `ETCD_VERIFY="all"` enviroment triggers [additional verification of consistency](https://github.com/etcd-io/etcd/pull/) of etcd data-dir files. +- Add [`etcd --enable-log-rotation`](https://github.com/etcd-io/etcd/pull/12774) boolean flag which enables log rotation if true. +- Add [`etcd --log-rotation-config-json`](https://github.com/etcd-io/etcd/pull/12774) flag which allows passthrough of JSON config to configure log rotation for a file output target. ### Package `runtime` - Optimize [`runtime.FDUsage` by removing unnecessary sorting](https://github.com/etcd-io/etcd/pull/12214). @@ -171,6 +173,8 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change. - Use `embed.Config.LogLevel` instead. - Add [`embed.Config.ZapLoggerBuilder`](https://github.com/etcd-io/etcd/pull/11147) to allow creating a custom zap logger. - Replace [global `*zap.Logger` with etcd server logger object](https://github.com/etcd-io/etcd/pull/12212). +- Add [`embed.Config.EnableLogRotation`](https://github.com/etcd-io/etcd/pull/12774) which enables log rotation if true. +- Add [`embed.Config.LogRotationConfigJSON`](https://github.com/etcd-io/etcd/pull/12774) to allow passthrough of JSON config to configure log rotation for a file output target. ### Package `clientv3` diff --git a/bill-of-materials.json b/bill-of-materials.json index 641cdab5c4c..8cf907000c2 100644 --- a/bill-of-materials.json +++ b/bill-of-materials.json @@ -638,6 +638,15 @@ } ] }, + { + "project": "gopkg.in/natefinch/lumberjack.v2", + "licenses": [ + { + "type": "MIT License", + "confidence": 1 + } + ] + }, { "project": "gopkg.in/yaml.v2", "licenses": [ diff --git a/etcdctl/go.sum b/etcdctl/go.sum index 4de8a6f2259..6a4c8b562f9 100644 --- a/etcdctl/go.sum +++ b/etcdctl/go.sum @@ -469,6 +469,7 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/go.sum b/go.sum index e626e8ff1dc..2bbcc950ebe 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -477,6 +478,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/server/embed/config.go b/server/embed/config.go index 83ff1921055..44bd39d4b39 100644 --- a/server/embed/config.go +++ b/server/embed/config.go @@ -68,6 +68,15 @@ const ( StdErrLogOutput = "stderr" StdOutLogOutput = "stdout" + // DefaultLogRotationConfig is the default configuration used for log rotation. + // Log rotation is disabled by default. + // MaxSize = 100 // MB + // MaxAge = 0 // days (no limit) + // MaxBackups = 0 // no limit + // LocalTime = false // use computers local time, UTC by default + // Compress = false // compress the rotated log in gzip format + DefaultLogRotationConfig = `{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}` + // DefaultStrictReconfigCheck is the default value for "--strict-reconfig-check" flag. // It's enabled by default. DefaultStrictReconfigCheck = true @@ -86,6 +95,7 @@ var ( ErrConflictBootstrapFlags = fmt.Errorf("multiple discovery or bootstrap flags are set. " + "Choose one of \"initial-cluster\", \"discovery\" or \"discovery-srv\"") ErrUnsetAdvertiseClientURLsFlag = fmt.Errorf("--advertise-client-urls is required when --listen-client-urls is set explicitly") + ErrLogRotationInvalidLogOutput = fmt.Errorf("--log-outputs requires a single file path when --log-rotate-config-json is defined") DefaultInitialAdvertisePeerURLs = "http://localhost:2380" DefaultAdvertiseClientURLs = "http://localhost:2379" @@ -320,7 +330,10 @@ type Config struct { // - file path to append server logs to. // It can be multiple when "Logger" is zap. LogOutputs []string `json:"log-outputs"` - + // EnableLogRotation enables log rotation of a single LogOutputs file target. + EnableLogRotation bool `json:"enable-log-rotation"` + // LogRotationConfigJSON is a passthrough allowing a log rotation JSON config to be passed directly. + LogRotationConfigJSON string `json:"log-rotation-config-json"` // ZapLoggerBuilder is used to build the zap logger. ZapLoggerBuilder func(*Config) error @@ -440,12 +453,14 @@ func NewConfig() *Config { PreVote: true, - loggerMu: new(sync.RWMutex), - logger: nil, - Logger: "zap", - LogOutputs: []string{DefaultLogOutput}, - LogLevel: logutil.DefaultLogLevel, - EnableGRPCGateway: true, + loggerMu: new(sync.RWMutex), + logger: nil, + Logger: "zap", + LogOutputs: []string{DefaultLogOutput}, + LogLevel: logutil.DefaultLogLevel, + EnableLogRotation: false, + LogRotationConfigJSON: DefaultLogRotationConfig, + EnableGRPCGateway: true, ExperimentalDowngradeCheckTime: DefaultDowngradeCheckTime, ExperimentalMemoryMlock: false, diff --git a/server/embed/config_logging.go b/server/embed/config_logging.go index c5a74014f1f..261949ef099 100644 --- a/server/embed/config_logging.go +++ b/server/embed/config_logging.go @@ -16,8 +16,11 @@ package embed import ( "crypto/tls" + "encoding/json" + "errors" "fmt" "io/ioutil" + "net/url" "os" "go.etcd.io/etcd/client/pkg/v3/logutil" @@ -26,6 +29,7 @@ import ( "go.uber.org/zap/zapgrpc" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" + "gopkg.in/natefinch/lumberjack.v2" ) // GetLogger returns the logger. @@ -54,6 +58,11 @@ func (cfg *Config) setupLogging() error { } } } + if cfg.EnableLogRotation { + if err := setupLogRotation(cfg.LogOutputs, cfg.LogRotationConfigJSON); err != nil { + return err + } + } outputPaths, errOutputPaths := make([]string, 0), make([]string, 0) isJournal := false @@ -75,8 +84,15 @@ func (cfg *Config) setupLogging() error { errOutputPaths = append(errOutputPaths, StdOutLogOutput) default: - outputPaths = append(outputPaths, v) - errOutputPaths = append(errOutputPaths, v) + var path string + if cfg.EnableLogRotation { + // append rotate scheme to logs managed by lumberjack log rotation + path = fmt.Sprintf("rotate:%s", v) + } else { + path = v + } + outputPaths = append(outputPaths, path) + errOutputPaths = append(errOutputPaths, path) } } @@ -211,3 +227,48 @@ func (cfg *Config) SetupGlobalLoggers() { zap.ReplaceGlobals(lg) } } + +type logRotationConfig struct { + *lumberjack.Logger +} + +// Sync implements zap.Sink +func (logRotationConfig) Sync() error { return nil } + +// setupLogRotation initializes log rotation for a single file path target. +func setupLogRotation(logOutputs []string, logRotateConfigJSON string) error { + var logRotationConfig logRotationConfig + outputFilePaths := 0 + for _, v := range logOutputs { + switch v { + case DefaultLogOutput, StdErrLogOutput, StdOutLogOutput: + continue + default: + outputFilePaths++ + } + } + // log rotation requires file target + if len(logOutputs) == 1 && outputFilePaths == 0 { + return ErrLogRotationInvalidLogOutput + } + // support max 1 file target for log rotation + if outputFilePaths > 1 { + return ErrLogRotationInvalidLogOutput + } + + if err := json.Unmarshal([]byte(logRotateConfigJSON), &logRotationConfig); err != nil { + var unmarshalTypeError *json.UnmarshalTypeError + var syntaxError *json.SyntaxError + switch { + case errors.As(err, &syntaxError): + return fmt.Errorf("improperly formatted log rotation config: %w", err) + case errors.As(err, &unmarshalTypeError): + return fmt.Errorf("invalid log rotation config: %w", err) + } + } + zap.RegisterSink("rotate", func(u *url.URL) (zap.Sink, error) { + logRotationConfig.Filename = u.Path + return &logRotationConfig, nil + }) + return nil +} diff --git a/server/embed/config_test.go b/server/embed/config_test.go index 846171c5e79..b86d0d6c246 100644 --- a/server/embed/config_test.go +++ b/server/embed/config_test.go @@ -15,6 +15,7 @@ package embed import ( + "errors" "fmt" "io/ioutil" "net" @@ -289,3 +290,77 @@ func TestPeerURLsMapAndTokenFromSRV(t *testing.T) { } } } + +func TestLogRotation(t *testing.T) { + tests := []struct { + name string + logOutputs []string + logRotationConfig string + wantErr bool + wantErrMsg error + }{ + { + name: "mixed log output targets", + logOutputs: []string{"stderr", "/tmp/path"}, + logRotationConfig: `{"maxsize": 1}`, + }, + { + name: "no file targets", + logOutputs: []string{"stderr"}, + logRotationConfig: `{"maxsize": 1}`, + wantErr: true, + wantErrMsg: ErrLogRotationInvalidLogOutput, + }, + { + name: "multiple file targets", + logOutputs: []string{"/tmp/path1", "/tmp/path2"}, + logRotationConfig: DefaultLogRotationConfig, + wantErr: true, + wantErrMsg: ErrLogRotationInvalidLogOutput, + }, + { + name: "default output", + logRotationConfig: `{"maxsize": 1}`, + wantErr: true, + wantErrMsg: ErrLogRotationInvalidLogOutput, + }, + { + name: "default log rotation config", + logOutputs: []string{"/tmp/path"}, + logRotationConfig: DefaultLogRotationConfig, + }, + { + name: "invalid logger config", + logOutputs: []string{"/tmp/path"}, + logRotationConfig: `{"maxsize": true}`, + wantErr: true, + wantErrMsg: errors.New("invalid log rotation config: json: cannot unmarshal bool into Go struct field logRotationConfig.maxsize of type int"), + }, + { + name: "improperly formatted logger config", + logOutputs: []string{"/tmp/path"}, + logRotationConfig: `{"maxsize": true`, + wantErr: true, + wantErrMsg: errors.New("improperly formatted log rotation config: unexpected end of JSON input"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := NewConfig() + cfg.Logger = "zap" + cfg.LogOutputs = tt.logOutputs + cfg.EnableLogRotation = true + cfg.LogRotationConfigJSON = tt.logRotationConfig + err := cfg.Validate() + if err != nil && !tt.wantErr { + t.Errorf("test %q, unexpected error %v", tt.name, err) + } + if err != nil && tt.wantErr && tt.wantErrMsg.Error() != err.Error() { + t.Errorf("test %q, expected error: %+v, got: %+v", tt.name, tt.wantErrMsg, err) + } + if err == nil && tt.wantErr { + t.Errorf("test %q, expected error, got nil", tt.name) + } + }) + } +} diff --git a/server/etcdmain/config.go b/server/etcdmain/config.go index 64ed7f1cbe7..a28d31ca9f0 100644 --- a/server/etcdmain/config.go +++ b/server/etcdmain/config.go @@ -236,6 +236,8 @@ func newConfig() *config { fs.StringVar(&cfg.ec.Logger, "logger", "zap", "Currently only supports 'zap' for structured logging.") fs.Var(flags.NewUniqueStringsValue(embed.DefaultLogOutput), "log-outputs", "Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets.") fs.StringVar(&cfg.ec.LogLevel, "log-level", logutil.DefaultLogLevel, "Configures log level. Only supports debug, info, warn, error, panic, or fatal. Default 'info'.") + fs.BoolVar(&cfg.ec.EnableLogRotation, "enable-log-rotation", false, "Enable log rotation of a single log-outputs file target.") + fs.StringVar(&cfg.ec.LogRotationConfigJSON, "log-rotation-config-json", embed.DefaultLogRotationConfig, "Configures log rotation if enabled with a JSON logger config. Default: MaxSize=100(MB), MaxAge=0(days,no limit), MaxBackups=0(no limit), LocalTime=false(UTC), Compress=false(gzip)") // version fs.BoolVar(&cfg.printVersion, "version", false, "Print the version and exit.") diff --git a/server/etcdmain/help.go b/server/etcdmain/help.go index 726c18cab68..8970254785f 100644 --- a/server/etcdmain/help.go +++ b/server/etcdmain/help.go @@ -188,6 +188,10 @@ Logging: Specify 'stdout' or 'stderr' to skip journald logging even when running under systemd, or list of comma separated output targets. --log-level 'info' Configures log level. Only supports debug, info, warn, error, panic, or fatal. + --enable-log-rotation 'false' + Enable log rotation of a single log-outputs file target. + --log-rotation-config-json '{"maxsize": 100, "maxage": 0, "maxbackups": 0, "localtime": false, "compress": false}' + Configures log rotation if enabled with a JSON logger config. MaxSize(MB), MaxAge(days,0=no limit), MaxBackups(0=no limit), LocalTime(use computers local time), Compress(gzip)". v2 Proxy (to be deprecated in v4): --proxy 'off' diff --git a/server/go.mod b/server/go.mod index 576f357fb09..3e8a8c92d55 100644 --- a/server/go.mod +++ b/server/go.mod @@ -40,6 +40,7 @@ require ( golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 google.golang.org/grpc v1.36.1 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 sigs.k8s.io/yaml v1.2.0 ) diff --git a/server/go.sum b/server/go.sum index bb8c2ccad18..ad5ded7aaff 100644 --- a/server/go.sum +++ b/server/go.sum @@ -463,6 +463,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/tests/go.sum b/tests/go.sum index d227410ddfc..2b2b5d136da 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -472,6 +472,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=