diff --git a/README.md b/README.md index aac8376..3970fc8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ If you have any difficulties in using cb-log, please let us know. | Configurations | Descriptions | Default | |:-------------:|:--------------|:-------------| - | loopcheck | 설정값 변경시 자동 반영 여부 설정.
설정값: true, false | false | | loglevel | 로그 레벨 설정.
설정값: trace, debug, info, warn, error, fatal, panic | info | | logfile | 로그 파일 출력 여부 설정.
설정값: true, false | true | | logfileinfo: | ----- 이하 logfile true 일때 유효 ----- || @@ -71,11 +70,8 @@ If you have any difficulties in using cb-log, please let us know. #### Config for CB-Log Lib. #### cblog: - ## true | false - loopcheck: false # This temp method for development is busy wait. cf) cblogger.go:levelSetupLoop(). - ## trace | debug | info | warn/warning | error | fatal | panic - loglevel: error # If loopcheck is true, You can set this online. + loglevel: error # The log level can be changed dynamically by editing this file. ## true | false logfile: true @@ -89,8 +85,7 @@ If you have any difficulties in using cb-log, please let us know. ``` - 설정 적용 방법 - - 서버 재가동: loopcheck=false 설정시 - - 자동 반영: loopcheck=true 설정시 + - 자동 반영: 설정 파일 수정시 즉시 반영 (파일 변경 이벤트 감지) - 설정파일 위치 지정 방법 - 환경변수 사용 방법: diff --git a/cblogger.go b/cblogger.go index e2303a7..ca362f9 100644 --- a/cblogger.go +++ b/cblogger.go @@ -10,10 +10,12 @@ package cblog import ( + "context" "os" - "time" + "path/filepath" cblogformatter "github.com/cloud-barista/cb-log/formatter" + "github.com/fsnotify/fsnotify" "github.com/sirupsen/logrus" "github.com/snowzach/rotatefilehook" ) @@ -28,6 +30,7 @@ var ( thisLogger *CBLogger thisFormatter *cblogformatter.Formatter cblogConfig CBLOGCONFIG + watcherCancel context.CancelFunc // stops the background watcher goroutine ) // Get the logger with name you set. The name will be used as below (name: CB-SPIDER) @@ -66,12 +69,10 @@ func setup(loggerName string, configFilePath string) { cblogConfig = GetConfigInfos(configFilePath) thisLogger.logrus.SetReportCaller(true) - if cblogConfig.CBLOG.LOOPCHECK { - SetLevel(cblogConfig.CBLOG.LOGLEVEL) - go levelSetupLoop(loggerName, configFilePath) - } else { - SetLevel(cblogConfig.CBLOG.LOGLEVEL) - } + SetLevel(cblogConfig.CBLOG.LOGLEVEL) + ctx, cancel := context.WithCancel(context.Background()) + watcherCancel = cancel + go levelSetupWatcher(ctx, loggerName, configFilePath) if cblogConfig.CBLOG.LOGFILE { setRotateFileHook(loggerName, &cblogConfig) @@ -88,17 +89,70 @@ func setup(loggerName string, configFilePath string) { } } -// Now, this method is busy wait. -// @TODO must change this with file watch&event. +// levelSetupWatcher watches the config file for changes using fsnotify +// and updates the log level whenever the file is modified. +// It stops when ctx is cancelled. // ref) https://github.com/fsnotify/fsnotify/blob/master/example_test.go -func levelSetupLoop(loggerName string, configFilePath string) { +func levelSetupWatcher(ctx context.Context, loggerName string, configFilePath string) { + // Resolve the config file path the same way GetConfigInfos does. + watchPath := configFilePath + if watchPath == "" { + cblogRootPath := os.Getenv("CBLOG_ROOT") + if cblogRootPath != "" { + watchPath = filepath.Join(cblogRootPath, "conf", "log_conf.yaml") + } + } + + if watchPath == "" { + logrus.Warn("[cb-log] No config file path could be determined; file watcher will not start.") + return + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + logrus.Errorf("[cb-log] Failed to create file watcher: %v", err) + return + } + defer watcher.Close() + + if err := watcher.Add(watchPath); err != nil { + logrus.Errorf("[cb-log] Failed to watch config file %s: %v", watchPath, err) + return + } + for { - cblogConfig = GetConfigInfos(configFilePath) - SetLevel(cblogConfig.CBLOG.LOGLEVEL) - time.Sleep(time.Second * 2) + select { + case <-ctx.Done(): + return + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { + reloadConfig(configFilePath) + } + // Re-add the watch when the file is renamed/removed (e.g. atomic saves by vim/emacs). + if event.Has(fsnotify.Rename) || event.Has(fsnotify.Remove) { + if err := watcher.Add(watchPath); err != nil { + logrus.Errorf("[cb-log] Failed to re-watch config file %s: %v", watchPath, err) + } + reloadConfig(configFilePath) + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + logrus.Errorf("[cb-log] File watcher error: %v", err) + } } } +// reloadConfig re-reads the config file and applies the new log level. +func reloadConfig(configFilePath string) { + cblogConfig = GetConfigInfos(configFilePath) + SetLevel(cblogConfig.CBLOG.LOGLEVEL) +} + func setRotateFileHook(loggerName string, logConfig *CBLOGCONFIG) { level, _ := logrus.ParseLevel(logConfig.CBLOG.LOGLEVEL) diff --git a/cblogger_test.go b/cblogger_test.go new file mode 100644 index 0000000..e28d0b7 --- /dev/null +++ b/cblogger_test.go @@ -0,0 +1,179 @@ +// CB-Log: Logger for Cloud-Barista. +// +// * Cloud-Barista: https://github.com/cloud-barista +// +// Unit tests for cblogger.go / config.go. +// Focus areas: +// 1. LOOPCHECK field is gone — struct and defaults reflect its removal. +// 2. A config file that still contains 'loopcheck: true' is parsed without +// error (unknown YAML fields are silently ignored for backward compatibility). +// 3. The file watcher fires automatically when the config file is modified — +// no 'loopcheck' flag is needed. +package cblog + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +// resetGlobals clears package-level singletons so each test starts clean. +// It cancels any running watcher goroutine before clearing the logger. +func resetGlobals() { + if watcherCancel != nil { + watcherCancel() + watcherCancel = nil + } + thisLogger = nil + thisFormatter = nil + cblogConfig = CBLOGCONFIG{} +} + +// writeConfig creates a temp directory tree and writes yaml to +// /conf/log_conf.yaml. It returns the root dir and the full file path. +func writeConfig(t *testing.T, yaml string) (dir, cfgPath string) { + t.Helper() + dir = t.TempDir() + confDir := filepath.Join(dir, "conf") + if err := os.MkdirAll(confDir, 0755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + cfgPath = filepath.Join(confDir, "log_conf.yaml") + if err := os.WriteFile(cfgPath, []byte(yaml), 0644); err != nil { + t.Fatalf("WriteFile: %v", err) + } + return dir, cfgPath +} + +// TestNewCBLOGCONFIG_Defaults checks that the default configuration no +// longer contains a LOOPCHECK field and that remaining defaults are correct. +func TestNewCBLOGCONFIG_Defaults(t *testing.T) { + cfg := NewCBLOGCONFIG() + if cfg.CBLOG.LOGLEVEL != "info" { + t.Errorf("expected default LOGLEVEL 'info', got '%s'", cfg.CBLOG.LOGLEVEL) + } + if !cfg.CBLOG.CONSOLE { + t.Error("expected default CONSOLE true") + } + if !cfg.CBLOG.LOGFILE { + t.Error("expected default LOGFILE true") + } + // Compilation itself is the strongest proof that LOOPCHECK is gone: + // any reference to cfg.CBLOG.LOOPCHECK would be a compile error here. +} + +// TestGetConfigInfos_WithoutLoopcheck loads a config YAML that does not +// contain the 'loopcheck' key and verifies the parsed values. +func TestGetConfigInfos_WithoutLoopcheck(t *testing.T) { + const yaml = ` +cblog: + loglevel: debug + console: true + logfile: false +logfileinfo: + filename: ./log/test.log + maxsize: 5 + maxbackups: 2 + maxage: 7 +` + dir, _ := writeConfig(t, yaml) + t.Setenv("CBLOG_ROOT", dir) + + cfg := GetConfigInfos("") + if cfg.CBLOG.LOGLEVEL != "debug" { + t.Errorf("expected loglevel 'debug', got '%s'", cfg.CBLOG.LOGLEVEL) + } + if cfg.CBLOG.LOGFILE { + t.Error("expected LOGFILE false") + } +} + +// TestGetConfigInfos_OldConfigWithLoopcheck verifies backward compatibility: +// a config YAML that still contains 'loopcheck: true' (old format) is loaded +// without error and the unknown field is silently ignored. +func TestGetConfigInfos_OldConfigWithLoopcheck(t *testing.T) { + const yaml = ` +cblog: + loopcheck: true + loglevel: warn + console: true + logfile: false +logfileinfo: + filename: ./log/test.log + maxsize: 5 + maxbackups: 2 + maxage: 7 +` + dir, _ := writeConfig(t, yaml) + t.Setenv("CBLOG_ROOT", dir) + + cfg := GetConfigInfos("") + if cfg.CBLOG.LOGLEVEL != "warn" { + t.Errorf("expected loglevel 'warn', got '%s'", cfg.CBLOG.LOGLEVEL) + } +} + +// TestDynamicLevelChange is the key end-to-end test: it verifies that editing +// the log level in the config file is picked up automatically by the +// always-on file watcher — no 'loopcheck' flag is required. +func TestDynamicLevelChange(t *testing.T) { + resetGlobals() + defer resetGlobals() + + const initialYAML = ` +cblog: + loglevel: info + console: false + logfile: false +logfileinfo: + filename: ./log/test.log + maxsize: 5 + maxbackups: 2 + maxage: 7 +` + dir, cfgPath := writeConfig(t, initialYAML) + t.Setenv("CBLOG_ROOT", dir) + + logger := GetLogger("TEST-WATCHER") + if logger == nil { + t.Fatal("expected non-nil logger") + } + if got := GetLevel(); got != "info" { + t.Fatalf("initial level: expected 'info', got '%s'", got) + } + + // Give the watcher goroutine time to call watcher.Add() and enter its + // select loop before we modify the file. + time.Sleep(200 * time.Millisecond) + + // Overwrite the config file with a different log level. + const updatedYAML = ` +cblog: + loglevel: error + console: false + logfile: false +logfileinfo: + filename: ./log/test.log + maxsize: 5 + maxbackups: 2 + maxage: 7 +` + if err := os.WriteFile(cfgPath, []byte(updatedYAML), 0644); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + // The file watcher is event-driven and should react within milliseconds; + // allow up to 3 s to account for slow CI runners. + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + if GetLevel() == "error" { + break + } + time.Sleep(100 * time.Millisecond) + } + + if got := GetLevel(); got != "error" { + t.Errorf("after file update: expected level 'error', got '%s'", got) + } +} diff --git a/conf/log_conf.yaml b/conf/log_conf.yaml index d8c0931..12a5495 100644 --- a/conf/log_conf.yaml +++ b/conf/log_conf.yaml @@ -1,12 +1,9 @@ #### Config for cb-log Lib. #### cblog: - ## true | false - loopcheck: false # This temp method for development is busy wait. cf) cblogger.go:levelSetupLoop(). - ## trace | debug | info | warn/warning | error | fatal | panic ## Default logging level: info - loglevel: info # If loopcheck is true, You can set this online. + loglevel: info # The log level can be changed dynamically by editing this file. ## true | false ## If true, log output to console. diff --git a/config.go b/config.go index 9157752..052963b 100644 --- a/config.go +++ b/config.go @@ -23,10 +23,9 @@ import ( type CBLOGCONFIG struct { CBLOG struct { - LOOPCHECK bool - LOGLEVEL string - CONSOLE bool - LOGFILE bool + LOGLEVEL string + CONSOLE bool + LOGFILE bool } LOGFILEINFO struct { @@ -40,7 +39,6 @@ type CBLOGCONFIG struct { func NewCBLOGCONFIG() CBLOGCONFIG { config := CBLOGCONFIG{} - config.CBLOG.LOOPCHECK = false config.CBLOG.LOGLEVEL = "info" config.CBLOG.CONSOLE = true config.CBLOG.LOGFILE = true diff --git a/go.mod b/go.mod index 92f9220..323f12b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/cloud-barista/cb-log go 1.25.0 require ( + github.com/fsnotify/fsnotify v1.9.0 github.com/sirupsen/logrus v1.9.1 github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d gopkg.in/yaml.v3 v3.0.1 @@ -10,7 +11,7 @@ require ( require ( github.com/BurntSushi/toml v0.4.1 // indirect - golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect + golang.org/x/sys v0.13.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 55aaef1..d9e0fcc 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= @@ -13,8 +15,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= diff --git a/test/test_loopcheck.go b/test/test_loopcheck.go index 46a593a..572785e 100644 --- a/test/test_loopcheck.go +++ b/test/test_loopcheck.go @@ -1,11 +1,22 @@ +// Dynamic log-level test: demonstrates that editing loglevel in the config +// file is picked up automatically by the always-on file watcher. +// +// Usage: +// +// export CBLOG_ROOT= +// cd $CBLOG_ROOT/test +// go run test_dynamic_loglevel.go +// +// While the program is running, edit $CBLOG_ROOT/conf/log_conf.yaml and +// change the 'loglevel' value. The new level will take effect immediately. package main import ( "fmt" "time" + cblog "github.com/cloud-barista/cb-log" "github.com/sirupsen/logrus" - "github.com/cloud-barista/cb-log" ) var cblogger *logrus.Logger @@ -23,7 +34,7 @@ func main() { cblogger.Warning("Log Waring message") cblogger.Error("Log Error message") cblogger.Errorf("Log Error message:%s", errorMsg()) - time.Sleep(time.Second*2) + time.Sleep(time.Second * 2) } }