diff options
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | plugins/logger/config.go | 92 | ||||
-rw-r--r-- | plugins/logger/enums.go | 12 | ||||
-rw-r--r-- | plugins/logger/zap_adapter.go | 2 | ||||
-rw-r--r-- | tests/plugins/http/configs/.rr-init.yaml | 6 | ||||
-rw-r--r-- | tests/plugins/logger/configs/.rr-file-logger.yaml | 23 | ||||
-rw-r--r-- | tests/plugins/logger/logger_test.go | 97 |
8 files changed, 218 insertions, 17 deletions
@@ -36,4 +36,5 @@ require ( golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 google.golang.org/protobuf v1.23.0 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -671,6 +671,8 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/plugins/logger/config.go b/plugins/logger/config.go index d2236cac..6ef56661 100644 --- a/plugins/logger/config.go +++ b/plugins/logger/config.go @@ -1,10 +1,12 @@ package logger import ( + "os" "strings" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" ) // ChannelConfig configures loggers per channel. @@ -13,9 +15,57 @@ type ChannelConfig struct { Channels map[string]Config `mapstructure:"channels"` } +// FileLoggerConfig structure represents configuration for the file logger +type FileLoggerConfig struct { + // Filename is the file to write logs to. Backup log files will be retained + // in the same directory. It uses <processname>-lumberjack.log in + // os.TempDir() if empty. + LogOutput string `mapstructure:"log_output"` + + // MaxSize is the maximum size in megabytes of the log file before it gets + // rotated. It defaults to 100 megabytes. + MaxSize int `mapstructure:"max_size"` + + // MaxAge is the maximum number of days to retain old log files based on the + // timestamp encoded in their filename. Note that a day is defined as 24 + // hours and may not exactly correspond to calendar days due to daylight + // savings, leap seconds, etc. The default is not to remove old log files + // based on age. + MaxAge int `mapstructure:"max_age"` + + // MaxBackups is the maximum number of old log files to retain. The default + // is to retain all old log files (though MaxAge may still cause them to get + // deleted.) + MaxBackups int `mapstructure:"max_backups"` + + // Compress determines if the rotated log files should be compressed + // using gzip. The default is not to perform compression. + Compress bool `mapstructure:"compress"` +} + +func (fl *FileLoggerConfig) InitDefaults() *FileLoggerConfig { + if fl.LogOutput == "" { + fl.LogOutput = os.TempDir() + } + + if fl.MaxSize == 0 { + fl.MaxSize = 100 + } + + if fl.MaxAge == 0 { + fl.MaxAge = 24 + } + + if fl.MaxBackups == 0 { + fl.MaxBackups = 10 + } + + return fl +} + type Config struct { // Mode configures logger based on some default template (development, production, off). - Mode string `mapstructure:"mode"` + Mode Mode `mapstructure:"mode"` // Level is the minimum enabled logging level. Note that this is a dynamic // level, so calling ChannelConfig.Level.SetLevel will atomically change the log @@ -38,17 +88,20 @@ type Config struct { // sends error-level logs to a different location from info- and debug-level // logs, see the package-level AdvancedConfiguration example. ErrorOutput []string `mapstructure:"errorOutput"` + + // File logger options + FileLogger *FileLoggerConfig `mapstructure:"file_logger_options"` } // BuildLogger converts config into Zap configuration. func (cfg *Config) BuildLogger() (*zap.Logger, error) { var zCfg zap.Config - switch strings.ToLower(cfg.Mode) { - case "off", "none": + switch Mode(strings.ToLower(string(cfg.Mode))) { + case off, none: return zap.NewNop(), nil - case "production": + case production: zCfg = zap.NewProductionConfig() - case "development": + case development: zCfg = zap.Config{ Level: zap.NewAtomicLevelAt(zap.DebugLevel), Development: true, @@ -72,7 +125,7 @@ func (cfg *Config) BuildLogger() (*zap.Logger, error) { OutputPaths: []string{"stderr"}, ErrorOutputPaths: []string{"stderr"}, } - case "raw": + case raw: zCfg = zap.Config{ Level: zap.NewAtomicLevelAt(zap.InfoLevel), Encoding: "console", @@ -120,13 +173,38 @@ func (cfg *Config) BuildLogger() (*zap.Logger, error) { zCfg.ErrorOutputPaths = cfg.ErrorOutput } + // if we also have a file logger specified in the config + // init it + // otherwise - return standard config + if cfg.FileLogger != nil { + // init absent options + cfg.FileLogger.InitDefaults() + + w := zapcore.AddSync( + &lumberjack.Logger{ + Filename: cfg.FileLogger.LogOutput, + MaxSize: cfg.FileLogger.MaxSize, + MaxAge: cfg.FileLogger.MaxAge, + MaxBackups: cfg.FileLogger.MaxBackups, + Compress: cfg.FileLogger.Compress, + }, + ) + + core := zapcore.NewCore( + zapcore.NewJSONEncoder(zCfg.EncoderConfig), + w, + zCfg.Level, + ) + return zap.New(core), nil + } + return zCfg.Build() } // InitDefault Initialize default logger func (cfg *Config) InitDefault() { if cfg.Mode == "" { - cfg.Mode = "development" + cfg.Mode = development } if cfg.Level == "" { cfg.Level = "debug" diff --git a/plugins/logger/enums.go b/plugins/logger/enums.go new file mode 100644 index 00000000..803eace0 --- /dev/null +++ b/plugins/logger/enums.go @@ -0,0 +1,12 @@ +package logger + +// Mode represents available logger modes +type Mode string + +const ( + none Mode = "none" + off Mode = "off" + production Mode = "production" + development Mode = "development" + raw Mode = "raw" +) diff --git a/plugins/logger/zap_adapter.go b/plugins/logger/zap_adapter.go index 6d865519..fab59844 100644 --- a/plugins/logger/zap_adapter.go +++ b/plugins/logger/zap_adapter.go @@ -10,7 +10,7 @@ type ZapAdapter struct { zl *zap.Logger } -// Create NewZapAdapter which uses general log interface +// NewZapAdapter ... which uses general log interface func NewZapAdapter(zapLogger *zap.Logger) *ZapAdapter { return &ZapAdapter{ zl: zapLogger.WithOptions(zap.AddCallerSkip(1)), diff --git a/tests/plugins/http/configs/.rr-init.yaml b/tests/plugins/http/configs/.rr-init.yaml index 1671c3c0..02cb1636 100644 --- a/tests/plugins/http/configs/.rr-init.yaml +++ b/tests/plugins/http/configs/.rr-init.yaml @@ -3,17 +3,13 @@ rpc: server: command: "php ../../http/client.php echo pipes" - user: "" - group: "" - env: - "RR_HTTP": "true" relay: "pipes" relay_timeout: "20s" http: address: 127.0.0.1:15395 max_request_size: 1024 - middleware: [ "" ] + middleware: [ ] uploads: forbid: [ ".php", ".exe", ".bat" ] trusted_subnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] diff --git a/tests/plugins/logger/configs/.rr-file-logger.yaml b/tests/plugins/logger/configs/.rr-file-logger.yaml new file mode 100644 index 00000000..49c30d02 --- /dev/null +++ b/tests/plugins/logger/configs/.rr-file-logger.yaml @@ -0,0 +1,23 @@ +server: + command: "php ../../http/client.php echo pipes" + relay: "pipes" + relay_timeout: "20s" + +http: + address: 127.0.0.1:54224 + max_request_size: 1024 + middleware: [ ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trusted_subnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + num_workers: 2 + max_jobs: 0 + allocate_timeout: 60s + destroy_timeout: 60s + +logs: + mode: development + level: debug + file_logger_options: + log_output: "test.log" diff --git a/tests/plugins/logger/logger_test.go b/tests/plugins/logger/logger_test.go index d2877781..9e3fa4da 100644 --- a/tests/plugins/logger/logger_test.go +++ b/tests/plugins/logger/logger_test.go @@ -1,16 +1,19 @@ package logger import ( + "net/http" "os" "os/signal" + "strings" "sync" "syscall" "testing" + "time" "github.com/golang/mock/gomock" endure "github.com/spiral/endure/pkg/container" "github.com/spiral/roadrunner/v2/plugins/config" - "github.com/spiral/roadrunner/v2/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/spiral/roadrunner/v2/plugins/logger" "github.com/spiral/roadrunner/v2/plugins/rpc" "github.com/spiral/roadrunner/v2/plugins/server" @@ -19,7 +22,7 @@ import ( ) func TestLogger(t *testing.T) { - container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + container, err := endure.NewContainer(nil, endure.RetryOnFail(false), endure.SetLogLevel(endure.ErrorLevel)) if err != nil { t.Fatal(err) } @@ -97,7 +100,7 @@ func TestLoggerRawErr(t *testing.T) { cfg, mockLogger, &server.Plugin{}, - &http.Plugin{}, + &httpPlugin.Plugin{}, ) assert.NoError(t, err) @@ -222,7 +225,7 @@ func TestLoggerNoConfig2(t *testing.T) { vp, &rpc.Plugin{}, &logger.ZapLogger{}, - &http.Plugin{}, + &httpPlugin.Plugin{}, &server.Plugin{}, ) assert.NoError(t, err) @@ -268,3 +271,89 @@ func TestLoggerNoConfig2(t *testing.T) { stopCh <- struct{}{} wg.Wait() } + +func TestFileLogger(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-file-logger.yaml" + vp.Prefix = "rr" + + err = container.RegisterAll( + vp, + &rpc.Plugin{}, + &logger.ZapLogger{}, + &httpPlugin.Plugin{}, + &server.Plugin{}, + ) + assert.NoError(t, err) + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + err = container.Stop() + assert.NoError(t, err) + return + case <-stopCh: + assert.NoError(t, container.Stop()) + return + } + } + }() + + time.Sleep(time.Second * 2) + t.Run("HTTPEchoReq", httpEcho) + + f, err := os.ReadFile("test.log") + if err != nil { + t.Fatal(err) + } + + strings.Contains(string(f), "worker constructed") + strings.Contains(string(f), "201 GET") + + _ = os.Remove("test.log") + + stopCh <- struct{}{} + wg.Wait() +} + +func httpEcho(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "http://localhost:54224?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusCreated, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} |