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)
}
}