fix: logging module

This commit is contained in:
mayur.popli
2025-03-17 23:56:12 +05:30
parent 9722c3bf68
commit 4ada80a361
5 changed files with 576 additions and 0 deletions

7
go.mod
View File

@@ -6,6 +6,13 @@ toolchain go1.23.7
require golang.org/x/crypto v0.36.0
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/rs/zerolog v1.33.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
)
require (
golang.org/x/sys v0.31.0 // indirect
gopkg.in/yaml.v2 v2.4.0

16
go.sum
View File

@@ -1,8 +1,24 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

150
log/log.go Normal file
View File

@@ -0,0 +1,150 @@
package logpackage
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/rs/zerolog"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/yaml.v2"
)
type LoggerConfig struct {
Level string `yaml:"level"`
FilePath string `yaml:"file_path"`
MaxSize int `yaml:"max_size"`
MaxBackups int `yaml:"max_backups"`
MaxAge int `yaml:"max_age"`
ContextKeys []string `yaml:"context_keys"`
}
var (
logger zerolog.Logger
cfg LoggerConfig
once sync.Once
getConfigPath = func() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("failed to get runtime caller")
}
dir := filepath.Dir(file)
return filepath.Join(dir, "log.yaml"), nil
}
)
func loadConfig() (LoggerConfig, error) {
var config LoggerConfig
configPath, err := getConfigPath()
if err != nil {
return config, fmt.Errorf("error finding config path: %w", err)
}
data, err := os.ReadFile(configPath)
if err != nil {
return config, fmt.Errorf("failed to read config file: %w", err)
}
err = yaml.Unmarshal(data, &config)
if err != nil {
return config, fmt.Errorf("failed to parse YAML: %w", err)
}
return config, nil
}
func InitLogger(configs ...LoggerConfig) {
once.Do(func() {
var err error
if len(configs) > 0 {
cfg = configs[0]
} else {
cfg, err = loadConfig()
if err != nil {
fmt.Println("Logger initialization failed:", err)
return
}
}
level, err := zerolog.ParseLevel(cfg.Level)
if err != nil {
level = zerolog.InfoLevel
}
zerolog.SetGlobalLevel(level)
zerolog.TimeFieldFormat = time.RFC3339
logWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
fileWriter := &lumberjack.Logger{
Filename: cfg.FilePath,
MaxSize: cfg.MaxSize,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAge,
}
multi := zerolog.MultiLevelWriter(logWriter, fileWriter)
logger = zerolog.New(multi).With().Timestamp().Logger()
})
}
func Debug(ctx context.Context, msg string) {
logEvent(ctx, logger.Debug(), msg)
}
func Debugf(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Debug(), fmt.Sprintf(format, v...))
}
func Info(ctx context.Context, msg string) {
logEvent(ctx, logger.Info(), msg)
}
func Infof(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Info(), fmt.Sprintf(format, v...))
}
func Warn(ctx context.Context, msg string) {
logEvent(ctx, logger.Warn(), msg)
}
func Warnf(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Warn(), fmt.Sprintf(format, v...))
}
func Error(ctx context.Context, err error, msg string) {
logEvent(ctx, logger.Error().Err(err), msg)
}
func Errorf(ctx context.Context, err error, format string, v ...any) {
logEvent(ctx, logger.Error().Err(err), fmt.Sprintf(format, v...))
}
var ExitFunc = func(code int) {
os.Exit(code)
}
func Fatal(ctx context.Context, msg string) {
logEvent(ctx, logger.Error(), msg)
ExitFunc(1)
}
func Fatalf(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Fatal(), fmt.Sprintf(format, v...))
ExitFunc(1)
}
func logEvent(ctx context.Context, event *zerolog.Event, msg string) {
for _, key := range cfg.ContextKeys {
if val, ok := ctx.Value(key).(string); ok && val != "" {
event.Str(key, val)
}
}
event.Msg(msg)
}

12
log/log.yaml Normal file
View File

@@ -0,0 +1,12 @@
level: info
destinations:
- type: stdout
- type: file
config:
path: logs/app.log
maxSize: "500"
maxBackups: "15"
maxAge: "30"
context_keys:
- requestId
- userId

391
log/logpackage_test.go Normal file
View File

@@ -0,0 +1,391 @@
package logpackage
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/rs/zerolog"
)
func setupTest(t *testing.T) string {
once = sync.Once{}
tempDir, err := os.MkdirTemp("", "logger-test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
return tempDir
}
func TestInitLoggerWithValidConfig(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
testLogPath := filepath.Join(tempDir, "test.log")
InitLogger(LoggerConfig{
Level: "debug",
FilePath: testLogPath,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
ContextKeys: []string{"request_id", "user_id"},
})
if logger.GetLevel() == zerolog.Disabled {
t.Error("Logger was not initialized")
}
ctx := context.WithValue(context.Background(), "request_id", "test-123")
Debug(ctx, "debug message")
Info(ctx, "info message")
Warn(ctx, "warning message")
Error(ctx, errors.New("test error"), "error message")
if _, err := os.Stat(testLogPath); os.IsNotExist(err) {
t.Errorf("Log file was not created at %s", testLogPath)
}
}
func TestInitLoggerWithInvalidLevel(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
testLogPath := filepath.Join(tempDir, "test.log")
InitLogger(LoggerConfig{
Level: "invalid_level",
FilePath: testLogPath,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
ContextKeys: []string{"request_id"},
})
if logger.GetLevel() == zerolog.Disabled {
t.Error("Logger was not initialized")
}
ctx := context.WithValue(context.Background(), "request_id", "test-123")
Info(ctx, "info message")
if _, err := os.Stat(testLogPath); os.IsNotExist(err) {
t.Errorf("Log file was not created at %s", testLogPath)
}
}
func TestLoadConfigFromFile(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
configPath := filepath.Join(tempDir, "log.yaml")
configContent := `level: debug
file_path: /tmp/test.log
max_size: 10
max_backups: 3
max_age: 7
context_keys:
- request_id
- user_id`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to write config file: %v", err)
}
originalGetConfigPath := getConfigPath
defer func() { getConfigPath = originalGetConfigPath }()
getConfigPath = func() (string, error) {
return configPath, nil
}
config, err := loadConfig()
if err != nil {
t.Errorf("loadConfig() error = %v", err)
}
if config.Level != "debug" {
t.Errorf("Expected level 'debug', got '%s'", config.Level)
}
if config.FilePath != "/tmp/test.log" {
t.Errorf("Expected file_path '/tmp/test.log', got '%s'", config.FilePath)
}
if len(config.ContextKeys) != 2 {
t.Errorf("Expected 2 context keys, got %d", len(config.ContextKeys))
}
}
func TestLoadConfigNonExistent(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
configPath := filepath.Join(tempDir, "non-existent.yaml")
originalGetConfigPath := getConfigPath
defer func() { getConfigPath = originalGetConfigPath }()
getConfigPath = func() (string, error) {
return configPath, nil
}
_, err := loadConfig()
if err == nil {
t.Error("Expected error for non-existent config file, got nil")
}
}
func TestLoadConfigInvalidYAML(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
configPath := filepath.Join(tempDir, "invalid.yaml")
configContent := `level: debug
file_path: /tmp/test.log
max_size: invalid
max_backups: 3`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to write config file: %v", err)
}
originalGetConfigPath := getConfigPath
defer func() { getConfigPath = originalGetConfigPath }()
getConfigPath = func() (string, error) {
return configPath, nil
}
_, err = loadConfig()
if err == nil {
t.Error("Expected error for invalid YAML, got nil")
}
}
func TestFatal(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
testLogPath := filepath.Join(tempDir, "fatal.log")
originalExitFunc := ExitFunc
defer func() { ExitFunc = originalExitFunc }()
var exitCalled bool
var exitCode int
ExitFunc = func(code int) {
exitCalled = true
exitCode = code
}
InitLogger(LoggerConfig{
Level: "debug",
FilePath: testLogPath,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
ContextKeys: []string{"request_id"},
})
ctx := context.WithValue(context.Background(), "request_id", "test-fatal")
Fatal(ctx, "fatal message")
if !exitCalled {
t.Error("ExitFunc was not called")
}
if exitCode != 1 {
t.Errorf("Expected exit code 1, got %d", exitCode)
}
if _, err := os.Stat(testLogPath); os.IsNotExist(err) {
t.Errorf("Log file was not created at %s", testLogPath)
}
content, err := os.ReadFile(testLogPath)
if err != nil {
t.Errorf("Failed to read log file: %v", err)
}
if !strings.Contains(string(content), "fatal message") {
t.Error("Log file does not contain fatal message")
}
}
func TestLoggingWithContext(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
testLogPath := filepath.Join(tempDir, "context.log")
// Initialize logger with context keys
InitLogger(LoggerConfig{
Level: "debug",
FilePath: testLogPath,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
ContextKeys: []string{"request_id", "user_id", "session_id"},
})
ctx := context.Background()
ctx = context.WithValue(ctx, "request_id", "req-123")
ctx = context.WithValue(ctx, "user_id", "user-456")
Info(ctx, "message with context")
if _, err := os.Stat(testLogPath); os.IsNotExist(err) {
t.Errorf("Log file was not created at %s", testLogPath)
}
content, err := os.ReadFile(testLogPath)
if err != nil {
t.Errorf("Failed to read log file: %v", err)
}
contentStr := string(content)
if !strings.Contains(contentStr, "req-123") {
t.Error("Log file does not contain request_id")
}
if !strings.Contains(contentStr, "user-456") {
t.Error("Log file does not contain user_id")
}
}
func TestFormattedLogging(t *testing.T) {
once = sync.Once{}
tempDir, err := os.MkdirTemp("", "logger-format-test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
testLogPath := filepath.Join(tempDir, "formatted.log")
InitLogger(LoggerConfig{
Level: "debug",
FilePath: testLogPath,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
ContextKeys: []string{"request_id"},
})
ctx := context.WithValue(context.Background(), "request_id", "format-test-123")
testValues := []struct {
name string
number int
text string
expected string
}{
{
name: "debug",
number: 42,
text: "formatted debug",
expected: "formatted debug message #42",
},
{
name: "info",
number: 100,
text: "formatted info",
expected: "formatted info message #100",
},
{
name: "warn",
number: 200,
text: "formatted warning",
expected: "formatted warning message #200",
},
{
name: "error",
number: 500,
text: "formatted error",
expected: "formatted error message #500",
},
}
for _, tv := range testValues {
t.Run(tv.name+"f", func(t *testing.T) {
format := "%s message #%d"
switch tv.name {
case "debug":
Debugf(ctx, format, tv.text, tv.number)
case "info":
Infof(ctx, format, tv.text, tv.number)
case "warn":
Warnf(ctx, format, tv.text, tv.number)
case "error":
testErr := errors.New("test error")
Errorf(ctx, testErr, format, tv.text, tv.number)
}
})
}
}
func TestLoggingWithNonStringContext(t *testing.T) {
tempDir := setupTest(t)
defer os.RemoveAll(tempDir)
testLogPath := filepath.Join(tempDir, "non-string.log")
InitLogger(LoggerConfig{
Level: "debug",
FilePath: testLogPath,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
ContextKeys: []string{"request_id", "count"},
})
ctx := context.Background()
ctx = context.WithValue(ctx, "request_id", "req-123")
ctx = context.WithValue(ctx, "count", 42)
Info(ctx, "message with non-string context")
if _, err := os.Stat(testLogPath); os.IsNotExist(err) {
t.Errorf("Log file was not created at %s", testLogPath)
}
content, err := os.ReadFile(testLogPath)
if err != nil {
t.Errorf("Failed to read log file: %v", err)
}
contentStr := string(content)
if !strings.Contains(contentStr, "req-123") {
t.Error("Log file does not contain request_id")
}
}
func TestGetConfigPath(t *testing.T) {
path, err := getConfigPath()
if err != nil {
t.Errorf("getConfigPath() error = %v", err)
}
if path == "" {
t.Error("getConfigPath() returned empty path")
}
}
func TestGetConfigPathError(t *testing.T) {
originalGetConfigPath := getConfigPath
defer func() { getConfigPath = originalGetConfigPath }()
expectedErr := errors.New("runtime caller error")
getConfigPath = func() (string, error) {
return "", expectedErr
}
once = sync.Once{}
InitLogger()
ctx := context.Background()
Info(ctx, "info after config failure")
}