Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ If you have any difficulties in using cb-log, please let us know.

| Configurations | Descriptions | Default |
|:-------------:|:--------------|:-------------|
| loopcheck | 설정값 변경시 자동 반영 여부 설정. <br>설정값: true, false | false |
| loglevel | 로그 레벨 설정. <br>설정값: trace, debug, info, warn, error, fatal, panic | info |
| logfile | 로그 파일 출력 여부 설정. <br>설정값: true, false | true |
| logfileinfo: | ----- 이하 logfile true 일때 유효 ----- ||
Expand All @@ -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
Expand All @@ -89,8 +85,7 @@ If you have any difficulties in using cb-log, please let us know.
```

- 설정 적용 방법
- 서버 재가동: loopcheck=false 설정시
- 자동 반영: loopcheck=true 설정시
- 자동 반영: 설정 파일 수정시 즉시 반영 (파일 변경 이벤트 감지)

- 설정파일 위치 지정 방법
- 환경변수 사용 방법:
Expand Down
80 changes: 67 additions & 13 deletions cblogger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
179 changes: 179 additions & 0 deletions cblogger_test.go
Original file line number Diff line number Diff line change
@@ -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
// <dir>/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)
}
}
5 changes: 1 addition & 4 deletions conf/log_conf.yaml
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 3 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ 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
)

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
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
Loading