feat: logging added

This commit is contained in:
mayur.popli
2025-03-18 11:05:15 +05:30
parent 4ada80a361
commit c315cd2c77
4 changed files with 468 additions and 485 deletions

View File

@@ -1,150 +1,251 @@
package logpackage package log
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"path/filepath" "strconv"
"runtime"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/yaml.v2" // "gopkg.in/yaml.v2"
) )
type LoggerConfig struct { type Level string
Level string `yaml:"level"` type DestinationType string
FilePath string `yaml:"file_path"` type Destination struct {
MaxSize int `yaml:"max_size"` Type DestinationType `yaml:"type"`
MaxBackups int `yaml:"max_backups"` Config map[string]string `yaml:"config"`
MaxAge int `yaml:"max_age"` }
ContextKeys []string `yaml:"context_keys"`
const (
Stdout DestinationType = "stdout"
File DestinationType = "file"
)
const (
DebugLevel Level = "debug"
InfoLevel Level = "info"
WarnLevel Level = "warn"
ErrorLevel Level = "error"
FatalLevel Level = "fatal"
PanicLevel Level = "panic"
)
var logLevels = map[Level]zerolog.Level{
DebugLevel: zerolog.DebugLevel,
InfoLevel: zerolog.InfoLevel,
WarnLevel: zerolog.WarnLevel,
ErrorLevel: zerolog.ErrorLevel,
FatalLevel: zerolog.FatalLevel,
PanicLevel: zerolog.PanicLevel,
}
type Config struct {
level Level `yaml:"level"`
destinations []Destination `yaml:"destinations"`
contextKeys []string `yaml:"contextKeys"`
} }
var ( var (
logger zerolog.Logger logger zerolog.Logger
cfg LoggerConfig cfg Config
once sync.Once 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 (
var config LoggerConfig ErrInvalidLogLevel = errors.New("invalid log level")
ErrLogDestinationNil = errors.New("log Destinations cant be empty")
ErrMissingFilePath = errors.New("file path missing in destination config for file logging")
)
configPath, err := getConfigPath() func (config *Config) validate() error {
if err != nil { if _, exists := logLevels[config.level]; !exists {
return config, fmt.Errorf("error finding config path: %w", err) return ErrInvalidLogLevel
} }
data, err := os.ReadFile(configPath) if len(config.destinations) == 0 {
if err != nil { return ErrLogDestinationNil
return config, fmt.Errorf("failed to read config file: %w", err)
} }
err = yaml.Unmarshal(data, &config) for _, dest := range config.destinations {
if err != nil { switch dest.Type {
return config, fmt.Errorf("failed to parse YAML: %w", err) case Stdout:
case File:
if _, exists := dest.Config["path"]; !exists {
return ErrMissingFilePath
} }
return config, nil for _, key := range []string{"maxSize", "maxBackups", "maxAge"} {
if valStr, ok := dest.Config[key]; ok {
if _, err := strconv.Atoi(valStr); err != nil {
return fmt.Errorf("invalid %s: %w", key, err)
}
}
}
default:
return fmt.Errorf("Invalid destination type '%s'", dest.Type)
}
}
return nil
} }
func InitLogger(configs ...LoggerConfig) { var defaultConfig = Config{
level: InfoLevel,
destinations: []Destination{
{Type: Stdout},
},
contextKeys: []string{"userID", "requestID"},
}
func init() {
logger, _ = getLogger(defaultConfig)
}
func getLogger(config Config) (zerolog.Logger, error) {
var newLogger zerolog.Logger
var writers []io.Writer
for _, dest := range config.destinations {
switch dest.Type {
case Stdout:
writers = append(writers, os.Stdout)
case File:
filePath := dest.Config["path"]
lumberjackLogger := &lumberjack.Logger{
Filename: filePath,
}
setConfigValue := func(key string, target *int) {
if valStr, ok := dest.Config[key]; ok {
if val, err := strconv.Atoi(valStr); err == nil {
*target = val
}
}
}
setConfigValue("maxSize", &lumberjackLogger.MaxSize)
setConfigValue("maxBackups", &lumberjackLogger.MaxBackups)
setConfigValue("maxAge", &lumberjackLogger.MaxAge)
if compress, ok := dest.Config["compress"]; ok {
lumberjackLogger.Compress = compress == "true"
}
writers = append(writers, lumberjackLogger)
}
}
multiwriter := io.MultiWriter(writers...)
newLogger = zerolog.New(multiwriter).
Level(logLevels[config.level]).
With().
Timestamp().
Caller().
Logger()
cfg = config
return newLogger, nil
}
func InitLogger(c Config) error {
if err := c.validate(); err != nil {
return err
}
var initErr error
once.Do(func() { once.Do(func() {
var err error logger, initErr = getLogger(c)
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()
}) })
return initErr
} }
func Debug(ctx context.Context, msg string) { func Debug(ctx context.Context, msg string) {
logEvent(ctx, logger.Debug(), msg) logEvent(ctx, zerolog.DebugLevel, msg, nil)
} }
func Debugf(ctx context.Context, format string, v ...any) { func Debugf(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Debug(), fmt.Sprintf(format, v...)) msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.DebugLevel, msg, nil)
} }
func Info(ctx context.Context, msg string) { func Info(ctx context.Context, msg string) {
logEvent(ctx, logger.Info(), msg) logEvent(ctx, zerolog.InfoLevel, msg, nil)
} }
func Infof(ctx context.Context, format string, v ...any) { func Infof(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Info(), fmt.Sprintf(format, v...)) msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.InfoLevel, msg, nil)
} }
func Warn(ctx context.Context, msg string) { func Warn(ctx context.Context, msg string) {
logEvent(ctx, logger.Warn(), msg) logEvent(ctx, zerolog.WarnLevel, msg, nil)
} }
func Warnf(ctx context.Context, format string, v ...any) { func Warnf(ctx context.Context, format string, v ...any) {
logEvent(ctx, logger.Warn(), fmt.Sprintf(format, v...)) msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.WarnLevel, msg, nil)
} }
func Error(ctx context.Context, err error, msg string) { func Error(ctx context.Context, err error, msg string) {
logEvent(ctx, logger.Error().Err(err), msg) logEvent(ctx, zerolog.ErrorLevel, msg, err)
} }
func Errorf(ctx context.Context, err error, format string, v ...any) { func Errorf(ctx context.Context, err error, format string, v ...any) {
logEvent(ctx, logger.Error().Err(err), fmt.Sprintf(format, v...)) msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.ErrorLevel, msg, err)
} }
var ExitFunc = func(code int) { func Fatal(ctx context.Context, err error, msg string) {
os.Exit(code) logEvent(ctx, zerolog.FatalLevel, msg, err)
} }
func Fatal(ctx context.Context, msg string) { func Fatalf(ctx context.Context, err error, format string, v ...any) {
logEvent(ctx, logger.Error(), msg) msg := fmt.Sprintf(format, v...)
ExitFunc(1) logEvent(ctx, zerolog.FatalLevel, msg, err)
} }
func Fatalf(ctx context.Context, format string, v ...any) { func Panic(ctx context.Context, err error, msg string) {
logEvent(ctx, logger.Fatal(), fmt.Sprintf(format, v...)) logEvent(ctx, zerolog.PanicLevel, msg, err)
ExitFunc(1)
} }
func logEvent(ctx context.Context, event *zerolog.Event, msg string) { func Panicf(ctx context.Context, err error, format string, v ...any) {
for _, key := range cfg.ContextKeys { msg := fmt.Sprintf(format, v...)
if val, ok := ctx.Value(key).(string); ok && val != "" { logEvent(ctx, zerolog.PanicLevel, msg, err)
}
func logEvent(ctx context.Context, level zerolog.Level, msg string, err error) {
event := logger.WithLevel(level)
if err != nil {
event = event.Err(err)
}
addCtx(ctx, event)
event.Msg(msg)
}
func Request(ctx context.Context, r *http.Request, body []byte) {
event := logger.Info()
addCtx(ctx, event)
event.Str("method", r.Method).
Str("url", r.URL.String()).
Str("body", string(body)).
Str("remoteAddr", r.RemoteAddr).
Msg("HTTP Request")
}
func addCtx(ctx context.Context, event *zerolog.Event) {
for _, key := range cfg.contextKeys {
val, ok := ctx.Value(key).(string)
if !ok {
continue
}
event.Str(key, val) event.Str(key, val)
} }
} }
event.Msg(msg)
func Response(ctx context.Context, r *http.Request, statusCode int, responseTime time.Duration) {
event := logger.Info()
addCtx(ctx, event)
event.Str("method", r.Method).
Str("url", r.URL.String()).
Int("statusCode", statusCode).
Dur("responseTime", responseTime).
Msg("HTTP Response")
} }

View File

@@ -3,10 +3,10 @@ destinations:
- type: stdout - type: stdout
- type: file - type: file
config: config:
path: logs/app.log path: my_log_file.log
maxSize: "500" maxSize: "500"
maxBackups: "15" maxBackups: "15"
maxAge: "30" maxAge: "30"
context_keys: contextKeys:
- requestId - requestID
- userId - userID

273
log/log_test.go Normal file
View File

@@ -0,0 +1,273 @@
package log
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"testing"
"time"
"github.com/rs/zerolog"
)
func TestLogFunctions(t *testing.T) {
testConfig := Config{
level: DebugLevel,
destinations: []Destination{
{Type: Stdout},
},
contextKeys: []string{"userID", "requestID"},
}
err := InitLogger(testConfig)
if err != nil {
t.Fatalf("Failed to initialize logger: %v", err)
}
tests := []struct {
name string
logFunc func(ctx context.Context)
expectedOutput string
}{
{
name: "Debug log with context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Debug(ctx, "debug message")
},
expectedOutput: `{"level":"debug","requestID":"12345","message":"debug message"}`,
},
{
name: "Debugf with context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Debugf(ctx, "formatted %s", "debug message")
},
expectedOutput: `{"level":"debug","requestID":"12345","message":"formatted debug message"}`,
},
{
name: "Info log with message",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Info(ctx, "info message")
},
expectedOutput: `{"level":"info","requestID":"12345","message":"info message"}`,
},
{
name: "Info log with formatted message",
logFunc: func(ctx context.Context) {
Infof(ctx, "formatted %s", "info message")
},
expectedOutput: `{"level":"info","message":"formatted info message"}`,
},
{
name: "Warn log with context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Warn(ctx, "warning message")
},
expectedOutput: `{"level":"warn","requestID":"12345","message":"warning message"}`,
},
{
name: "Warnf with context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Warnf(ctx, "formatted %s", "warning message")
},
expectedOutput: `{"level":"warn","requestID":"12345","message":"formatted warning message"}`,
},
{
name: "Error log with error and context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "userID", "67890")
Error(ctx, errors.New("something went wrong"), "error message")
},
expectedOutput: `{"level":"error","userID":"67890","error":"something went wrong","message":"error message"}`,
},
{
name: "Errorf with error and context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "userID", "67890")
Errorf(ctx, errors.New("something went wrong"), "formatted %s", "error message")
},
expectedOutput: `{"level":"error","userID":"67890","error":"something went wrong","message":"formatted error message"}`,
},
{
name: "Fatal log with error and context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Fatal(ctx, errors.New("fatal error"), "fatal message")
},
expectedOutput: `{"level":"fatal","requestID":"12345","error":"fatal error","message":"fatal message"}`,
},
{
name: "Fatalf with error and context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "requestID", "12345")
Fatalf(ctx, errors.New("fatal error"), "formatted %s", "fatal message")
},
expectedOutput: `{"level":"fatal","requestID":"12345","error":"fatal error","message":"formatted fatal message"}`,
},
{
name: "Panic log with error and context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "userID", "67890")
Panic(ctx, errors.New("panic error"), "panic message")
},
expectedOutput: `{"level":"panic","userID":"67890","error":"panic error","message":"panic message"}`,
},
{
name: "Panicf with error and context",
logFunc: func(ctx context.Context) {
ctx = context.WithValue(ctx, "userID", "67890")
Panicf(ctx, errors.New("panic error"), "formatted %s", "panic message")
},
expectedOutput: `{"level":"panic","userID":"67890","error":"panic error","message":"formatted panic message"}`,
},
{
name: "Request log",
logFunc: func(ctx context.Context) {
req, _ := http.NewRequest("GET", "http://example.com", nil)
req.RemoteAddr = "127.0.0.1:8080"
Request(ctx, req, []byte("request body"))
},
expectedOutput: `{"level":"info","method":"GET","url":"http://example.com","body":"request body","remoteAddr":"127.0.0.1:8080","message":"HTTP Request"}`,
},
{
name: "Response log",
logFunc: func(ctx context.Context) {
req, _ := http.NewRequest("GET", "http://example.com", nil)
Response(ctx, req, 200, 100*time.Millisecond)
},
expectedOutput: `{"level":"info","method":"GET","url":"http://example.com","statusCode":200,"responseTime":100,"message":"HTTP Response"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
logger = zerolog.New(&buf).With().Timestamp().Logger()
tt.logFunc(context.Background())
output := buf.String()
t.Logf("Log output: %s", output)
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) == 0 {
t.Fatal("No log output found")
}
lastLine := lines[len(lines)-1]
var logOutput map[string]interface{}
if err := json.Unmarshal([]byte(lastLine), &logOutput); err != nil {
t.Fatalf("Failed to unmarshal log output: %v", err)
}
delete(logOutput, "time")
delete(logOutput, "caller")
var expectedOutput map[string]interface{}
if err := json.Unmarshal([]byte(tt.expectedOutput), &expectedOutput); err != nil {
t.Fatalf("Failed to unmarshal expected output: %v", err)
}
for key, expectedValue := range expectedOutput {
actualValue, ok := logOutput[key]
if !ok {
t.Errorf("Expected key %q not found in log output", key)
continue
}
if actualValue != expectedValue {
t.Errorf("Mismatch for key %q: expected %v, got %v", key, expectedValue, actualValue)
}
}
})
}
}
func TestDestinationValidation(t *testing.T) {
tests := []struct {
name string
config Config
expectedError error
}{
// Missing `path` for File destination
{
name: "Missing file path",
config: Config{
level: InfoLevel,
destinations: []Destination{
{
Type: File,
Config: map[string]string{
"maxSize": "500",
"maxBackups": "15",
"maxAge": "30",
},
},
},
},
expectedError: ErrMissingFilePath,
},
{
name: "Invalid maxAge",
config: Config{
level: InfoLevel,
destinations: []Destination{
{
Type: File,
Config: map[string]string{
"path": "log/app.log",
"maxSize": "500",
"maxBackups": "15",
"maxAge": "invalid",
},
},
},
},
expectedError: errors.New("invalid maxAge"),
},
// Valid File destination (no error expected)
{
name: "Valid file destination",
config: Config{
level: InfoLevel,
destinations: []Destination{
{
Type: File,
Config: map[string]string{
"path": "log/app.log",
"maxSize": "500",
"maxBackups": "15",
"maxAge": "30",
},
},
},
},
expectedError: nil,
},
// Invalid destination type
{
name: "Invalid destination type",
config: Config{
level: InfoLevel,
destinations: []Destination{
{
Type: "invalid",
},
},
},
expectedError: errors.New("Invalid destination type 'invalid'"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InitLogger(tt.config)
if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) {
t.Errorf("Expected error: %v, got: %v", tt.expectedError, err)
} else if err != nil && tt.expectedError != nil && !strings.Contains(err.Error(), tt.expectedError.Error()) {
t.Errorf("Expected error to contain: %v, got: %v", tt.expectedError, err)
}
})
}
}

View File

@@ -1,391 +0,0 @@
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")
}