From d6390393aba04d073fc7e575569439c321a4236b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 01:49:14 +0000
Subject: [PATCH 1/5] Initial plan
From a88c53d09a94c1584687235f5e0f348d1e2e97d6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 01:50:35 +0000
Subject: [PATCH 2/5] Add fsnotify dependency
Co-authored-by: powerkimhub <46367962+powerkimhub@users.noreply.github.com>
---
go.mod | 3 ++-
go.sum | 4 ++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/go.mod b/go.mod
index 92f9220..44bd18b 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,8 @@ require (
require (
github.com/BurntSushi/toml v0.4.1 // indirect
- golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // 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..11d7f4a 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=
@@ -15,6 +17,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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=
From 5de033c47d068262c66f56755dcea288a94c1ced Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 01:53:17 +0000
Subject: [PATCH 3/5] Replace busy-wait config polling loop with fsnotify file
watcher
Co-authored-by: powerkimhub <46367962+powerkimhub@users.noreply.github.com>
---
cblogger.go | 61 ++++++++++++++++++++++++++++++++++++++++------
conf/log_conf.yaml | 2 +-
go.mod | 2 +-
go.sum | 2 --
4 files changed, 55 insertions(+), 12 deletions(-)
diff --git a/cblogger.go b/cblogger.go
index e2303a7..abf150e 100644
--- a/cblogger.go
+++ b/cblogger.go
@@ -11,9 +11,10 @@ package cblog
import (
"os"
- "time"
+ "path/filepath"
cblogformatter "github.com/cloud-barista/cb-log/formatter"
+ "github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/snowzach/rotatefilehook"
)
@@ -68,7 +69,7 @@ func setup(loggerName string, configFilePath string) {
if cblogConfig.CBLOG.LOOPCHECK {
SetLevel(cblogConfig.CBLOG.LOGLEVEL)
- go levelSetupLoop(loggerName, configFilePath)
+ go levelSetupWatcher(loggerName, configFilePath)
} else {
SetLevel(cblogConfig.CBLOG.LOGLEVEL)
}
@@ -88,14 +89,58 @@ 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.
// ref) https://github.com/fsnotify/fsnotify/blob/master/example_test.go
-func levelSetupLoop(loggerName string, configFilePath string) {
+func levelSetupWatcher(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] LOOPCHECK is enabled but 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 event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+ if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
+ cblogConfig = GetConfigInfos(configFilePath)
+ SetLevel(cblogConfig.CBLOG.LOGLEVEL)
+ }
+ // 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) {
+ _ = watcher.Add(watchPath)
+ cblogConfig = GetConfigInfos(configFilePath)
+ SetLevel(cblogConfig.CBLOG.LOGLEVEL)
+ }
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ logrus.Errorf("[cb-log] File watcher error: %v", err)
+ }
}
}
diff --git a/conf/log_conf.yaml b/conf/log_conf.yaml
index d8c0931..0aa1051 100644
--- a/conf/log_conf.yaml
+++ b/conf/log_conf.yaml
@@ -2,7 +2,7 @@
cblog:
## true | false
- loopcheck: false # This temp method for development is busy wait. cf) cblogger.go:levelSetupLoop().
+ loopcheck: false # If true, the logger watches the config file for changes and updates the log level dynamically.
## trace | debug | info | warn/warning | error | fatal | panic
## Default logging level: info
diff --git a/go.mod b/go.mod
index 44bd18b..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,6 @@ require (
require (
github.com/BurntSushi/toml v0.4.1 // indirect
- github.com/fsnotify/fsnotify v1.9.0 // 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 11d7f4a..d9e0fcc 100644
--- a/go.sum
+++ b/go.sum
@@ -15,8 +15,6 @@ 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=
From 7fccc74e20c285ed4906f29b54ec6271cbd53645 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 02:03:48 +0000
Subject: [PATCH 4/5] =?UTF-8?q?Remove=20loopcheck=20config=20option=20?=
=?UTF-8?q?=E2=80=94=20file=20watcher=20always=20runs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: powerkimhub <46367962+powerkimhub@users.noreply.github.com>
---
README.md | 9 ++-------
cblogger.go | 14 ++++++--------
conf/log_conf.yaml | 5 +----
config.go | 8 +++-----
4 files changed, 12 insertions(+), 24 deletions(-)
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 abf150e..4321c58 100644
--- a/cblogger.go
+++ b/cblogger.go
@@ -67,12 +67,8 @@ func setup(loggerName string, configFilePath string) {
cblogConfig = GetConfigInfos(configFilePath)
thisLogger.logrus.SetReportCaller(true)
- if cblogConfig.CBLOG.LOOPCHECK {
- SetLevel(cblogConfig.CBLOG.LOGLEVEL)
- go levelSetupWatcher(loggerName, configFilePath)
- } else {
- SetLevel(cblogConfig.CBLOG.LOGLEVEL)
- }
+ SetLevel(cblogConfig.CBLOG.LOGLEVEL)
+ go levelSetupWatcher(loggerName, configFilePath)
if cblogConfig.CBLOG.LOGFILE {
setRotateFileHook(loggerName, &cblogConfig)
@@ -103,7 +99,7 @@ func levelSetupWatcher(loggerName string, configFilePath string) {
}
if watchPath == "" {
- logrus.Warn("[cb-log] LOOPCHECK is enabled but no config file path could be determined; file watcher will not start.")
+ logrus.Warn("[cb-log] No config file path could be determined; file watcher will not start.")
return
}
@@ -131,7 +127,9 @@ func levelSetupWatcher(loggerName string, configFilePath string) {
}
// 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) {
- _ = watcher.Add(watchPath)
+ if err := watcher.Add(watchPath); err != nil {
+ logrus.Errorf("[cb-log] Failed to re-watch config file %s: %v", watchPath, err)
+ }
cblogConfig = GetConfigInfos(configFilePath)
SetLevel(cblogConfig.CBLOG.LOGLEVEL)
}
diff --git a/conf/log_conf.yaml b/conf/log_conf.yaml
index 0aa1051..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 # If true, the logger watches the config file for changes and updates the log level dynamically.
-
## 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
From 6ace644ff2937cfd95896ecb0bd335aa5ead2164 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 20 Mar 2026 02:15:37 +0000
Subject: [PATCH 5/5] Add unit tests; add context-based watcher cancellation;
extract reloadConfig helper
Co-authored-by: powerkimhub <46367962+powerkimhub@users.noreply.github.com>
---
cblogger.go | 23 ++++--
cblogger_test.go | 179 +++++++++++++++++++++++++++++++++++++++++
test/test_loopcheck.go | 15 +++-
3 files changed, 209 insertions(+), 8 deletions(-)
create mode 100644 cblogger_test.go
diff --git a/cblogger.go b/cblogger.go
index 4321c58..ca362f9 100644
--- a/cblogger.go
+++ b/cblogger.go
@@ -10,6 +10,7 @@
package cblog
import (
+ "context"
"os"
"path/filepath"
@@ -29,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)
@@ -68,7 +70,9 @@ func setup(loggerName string, configFilePath string) {
thisLogger.logrus.SetReportCaller(true)
SetLevel(cblogConfig.CBLOG.LOGLEVEL)
- go levelSetupWatcher(loggerName, configFilePath)
+ ctx, cancel := context.WithCancel(context.Background())
+ watcherCancel = cancel
+ go levelSetupWatcher(ctx, loggerName, configFilePath)
if cblogConfig.CBLOG.LOGFILE {
setRotateFileHook(loggerName, &cblogConfig)
@@ -87,8 +91,9 @@ func setup(loggerName string, configFilePath string) {
// 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 levelSetupWatcher(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 == "" {
@@ -117,21 +122,21 @@ func levelSetupWatcher(loggerName string, configFilePath string) {
for {
select {
+ case <-ctx.Done():
+ return
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
- cblogConfig = GetConfigInfos(configFilePath)
- SetLevel(cblogConfig.CBLOG.LOGLEVEL)
+ 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)
}
- cblogConfig = GetConfigInfos(configFilePath)
- SetLevel(cblogConfig.CBLOG.LOGLEVEL)
+ reloadConfig(configFilePath)
}
case err, ok := <-watcher.Errors:
if !ok {
@@ -142,6 +147,12 @@ func levelSetupWatcher(loggerName string, configFilePath string) {
}
}
+// 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
+//