Added Log package

Ft/logging module
Merge pull request #426 from MayurWitsLab/ft/logging_module
This commit is contained in:
shreyvishal
2025-03-28 20:43:32 +05:30
4 changed files with 966 additions and 1 deletions

13
go.mod
View File

@@ -10,11 +10,18 @@ require (
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
toolchain go1.23.7
require (
github.com/rs/zerolog v1.33.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.10.0
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03
golang.org/x/text v0.23.0 // indirect
golang.org/x/crypto v0.36.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
@@ -36,7 +43,11 @@ require (
require golang.org/x/text v0.23.0 // indirect
require golang.org/x/sys v0.31.0 // indirect
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/sys v0.31.0 // indirect
)
require (
cloud.google.com/go/pubsub v1.48.0
gopkg.in/yaml.v2 v2.4.0

16
go.sum
View File

@@ -35,10 +35,24 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+x
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 h1:m1h+vudopHsI67FPT9MOncyndWhTcdUoBtI1R1uajGY=
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03/go.mod h1:8sheVFH84v3PCyFY/O02mIgSQY9I6wMYPWsq7mDnEZY=
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=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
@@ -50,3 +64,5 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=

271
pkg/log/log.go Normal file
View File

@@ -0,0 +1,271 @@
package log
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/rs/zerolog"
"gopkg.in/natefinch/lumberjack.v2"
)
type level string
type destinationType string
type destination struct {
Type destinationType `yaml:"type"`
Config map[string]string `yaml:"config"`
}
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 (
logger zerolog.Logger
cfg Config
once sync.Once
)
var (
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")
)
func (config *Config) validate() error {
if _, exists := logLevels[config.Level]; !exists {
return ErrInvalidLogLevel
}
if len(config.Destinations) == 0 {
return ErrLogDestinationNil
}
for _, dest := range config.Destinations {
switch dest.Type {
case Stdout:
case File:
if _, exists := dest.Config["path"]; !exists {
return ErrMissingFilePath
}
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
}
var defaultConfig = Config{
Level: InfoLevel,
Destinations: []destination{
{Type: Stdout},
},
}
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"]
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return newLogger, fmt.Errorf("failed to create log directory: %v", err)
}
lumberjackLogger := &lumberjack.Logger{
Filename: filePath,
Compress: false,
}
absPath, err := filepath.Abs(filePath)
if err != nil {
return newLogger, fmt.Errorf("failed to get absolute path: %v", err)
}
lumberjackLogger.Filename = absPath
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...)
defer func() {
if closer, ok := multiwriter.(io.Closer); ok {
closer.Close()
}
}()
newLogger = zerolog.New(multiwriter).
Level(logLevels[config.Level]).
With().
Timestamp().
Logger()
cfg = config
return newLogger, nil
}
func InitLogger(c Config) error {
var initErr error
once.Do(func() {
if initErr = c.validate(); initErr != nil {
return
}
logger, initErr = getLogger(c)
})
return initErr
}
func Debug(ctx context.Context, msg string) {
logEvent(ctx, zerolog.DebugLevel, msg, nil)
}
func Debugf(ctx context.Context, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.DebugLevel, msg, nil)
}
func Info(ctx context.Context, msg string) {
logEvent(ctx, zerolog.InfoLevel, msg, nil)
}
func Infof(ctx context.Context, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.InfoLevel, msg, nil)
}
func Warn(ctx context.Context, msg string) {
logEvent(ctx, zerolog.WarnLevel, msg, nil)
}
func Warnf(ctx context.Context, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.WarnLevel, msg, nil)
}
func Error(ctx context.Context, err error, msg string) {
logEvent(ctx, zerolog.ErrorLevel, msg, err)
}
func Errorf(ctx context.Context, err error, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.ErrorLevel, msg, err)
}
func Fatal(ctx context.Context, err error, msg string) {
logEvent(ctx, zerolog.FatalLevel, msg, err)
}
func Fatalf(ctx context.Context, err error, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
logEvent(ctx, zerolog.FatalLevel, msg, err)
}
func Panic(ctx context.Context, err error, msg string) {
logEvent(ctx, zerolog.PanicLevel, msg, err)
}
func Panicf(ctx context.Context, err error, format string, v ...any) {
msg := fmt.Sprintf(format, v...)
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
}
keyStr := key
event.Any(keyStr, val)
}
}
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")
}

667
pkg/log/log_test.go Normal file
View File

@@ -0,0 +1,667 @@
package log
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
type ctxKey any
var requestID ctxKey = "requestID"
var userID ctxKey = "userID"
const testLogFilePath = "./test_logs/test.log"
func TestMain(m *testing.M) {
// Create a single temporary directory for all tests
var err error
dir := filepath.Dir(testLogFilePath)
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic("failed to create test log directory: " + err.Error())
}
// Run all tests
code := m.Run()
// Cleanup: Remove the log directory after all tests finish
err = os.RemoveAll(dir)
if err != nil {
println("failed to clean up test log directory: ", err.Error())
}
// Exit with the appropriate exit code
os.Exit(code)
}
func setupLogger(t *testing.T, l level) string {
t.Helper()
// Create a temporary directory for logs.
config := Config{
Level: l,
Destinations: []destination{
{
Type: File,
Config: map[string]string{
"path": testLogFilePath,
"maxSize": "1",
"maxAge": "1",
"maxBackup": "1",
"compress": "false",
},
},
},
ContextKeys: []string{"userID", "requestID"},
}
// Initialize logger with the given config
err := InitLogger(config)
if err != nil {
t.Fatalf("failed to initialize logger: %v", err)
}
return testLogFilePath
}
func readLogFile(t *testing.T, logPath string) []string {
t.Helper()
b, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("failed to read log file: %v", err)
}
return strings.Split(string(b), "\n")
}
func parseLogLine(t *testing.T, line string) map[string]interface{} {
t.Helper()
var logEntry map[string]interface{}
err := json.Unmarshal([]byte(line), &logEntry)
if err != nil {
t.Fatalf("Failed to parse log line: %v", err)
}
return logEntry
}
func TestDebug(t *testing.T) {
t.Helper()
logPath := setupLogger(t, DebugLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Debug(ctx, "Debug message")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "debug",
"userID": "12345",
"message": "Debug message",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("Expected Debug message, but it was not found in logs")
}
}
func TestInfo(t *testing.T) {
logPath := setupLogger(t, InfoLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Info(ctx, "Info message")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "info",
"userID": "12345",
"message": "Info message",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Info message, but it was not found in logs")
}
}
func TestWarn(t *testing.T) {
logPath := setupLogger(t, WarnLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Warn(ctx, "Warning message")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "warn",
"userID": "12345",
"message": "Warning message",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Warning message, but it was not found in logs")
}
}
func TestError(t *testing.T) {
logPath := setupLogger(t, ErrorLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Error(ctx, fmt.Errorf("test error"), "Error message")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "error",
"userID": "12345",
"message": "Error message",
"error": "test error",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Error message, but it was not found in logs")
}
}
func TestRequest(t *testing.T) {
logPath := setupLogger(t, InfoLevel)
ctx := context.WithValue(context.Background(), requestID, "abc-123")
req, _ := http.NewRequest("POST", "/api/test", bytes.NewBuffer([]byte(`{"key":"value"}`)))
req.RemoteAddr = "127.0.0.1:8080"
Request(ctx, req, []byte(`{"key":"value"}`))
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
if logEntry["message"] == "HTTP Request" || logEntry["method"] == "POST" {
found = true
break
}
}
if !found {
t.Errorf("expected formatted debug message, but it was not found in logs")
}
}
func TestResponse(t *testing.T) {
logPath := setupLogger(t, InfoLevel)
ctx := context.WithValue(context.Background(), requestID, "abc-123")
req, _ := http.NewRequest("GET", "/api/test", nil)
Response(ctx, req, 200, time.Millisecond*123)
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
if logEntry["message"] == "HTTP Response" {
if logEntry["message"] == "HTTP Response" {
value, ok := logEntry["statusCode"]
if !ok {
t.Fatalf("Expected key 'statusCode' not found in log entry")
}
statusCode, ok := value.(float64)
if !ok {
t.Fatalf("Value for 'statusCode' is not a float64, found: %T", value)
}
if statusCode == 200 {
found = true
break
}
}
}
}
if !found {
t.Errorf("expected message, but it was not found in logs")
}
}
func TestFatal(t *testing.T) {
logPath := setupLogger(t, FatalLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Fatal(ctx, fmt.Errorf("fatal error"), "Fatal message")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "fatal",
"userID": "12345",
"message": "Fatal message",
"error": "fatal error",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Fatal message, but it was not found in logs")
}
}
func TestPanic(t *testing.T) {
logPath := setupLogger(t, PanicLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Panic(ctx, fmt.Errorf("panic error"), "Panic message")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "panic",
"userID": "12345",
"message": "Panic message",
"error": "panic error",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Panic message, but it was not found in logs")
}
}
func TestDebugf(t *testing.T) {
logPath := setupLogger(t, DebugLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Debugf(ctx, "Debugf message: %s", "test")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "debug",
"userID": "12345",
"message": "Debugf message: test",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
t.Log(line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected formatted debug message, but it was not found in logs")
}
}
func TestInfof(t *testing.T) {
logPath := setupLogger(t, InfoLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Infof(ctx, "Infof message: %s", "test")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "info",
"userID": "12345",
"message": "Infof message: test",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Infof message, but it was not found in logs")
}
}
func TestWarnf(t *testing.T) {
logPath := setupLogger(t, WarnLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
Warnf(ctx, "Warnf message: %s", "test")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "warn",
"userID": "12345",
"message": "Warnf message: test",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Warnf message, but it was not found in logs")
}
}
func TestErrorf(t *testing.T) {
logPath := setupLogger(t, ErrorLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
err := fmt.Errorf("error message")
Errorf(ctx, err, "Errorf message: %s", "test")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "error",
"userID": "12345",
"message": "Errorf message: test",
"error": "error message",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Errorf message, but it was not found in logs")
}
}
func TestFatalf(t *testing.T) {
logPath := setupLogger(t, FatalLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
err := fmt.Errorf("fatal error")
Fatalf(ctx, err, "Fatalf message: %s", "test")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "fatal",
"userID": "12345",
"message": "Fatalf message: test",
"error": "fatal error",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Fatalf message, but it was not found in logs")
}
}
func TestPanicf(t *testing.T) {
logPath := setupLogger(t, PanicLevel)
ctx := context.WithValue(context.Background(), userID, "12345")
err := fmt.Errorf("panic error")
Panicf(ctx, err, "Panicf message: %s", "test")
lines := readLogFile(t, logPath)
if len(lines) == 0 {
t.Fatal("No logs were written.")
}
expected := map[string]interface{}{
"level": "panic",
"userID": "12345",
"message": "Panicf message: test",
"error": "panic error",
}
var found bool
for _, line := range lines {
logEntry := parseLogLine(t, line)
// Ignore 'time' while comparing
delete(logEntry, "time")
if reflect.DeepEqual(expected, logEntry) {
found = true
break
}
}
if !found {
t.Errorf("expected Panicf message, but it was not found in logs")
}
}
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
config Config
wantErr error
}{
{
name: "Valid config with Stdout",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{Type: Stdout},
},
},
wantErr: nil,
},
{
name: "Valid config with File destination and valid path",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{
Type: File,
Config: map[string]string{
"path": "./logs/app.log",
"maxSize": "10",
"maxBackups": "5",
"maxAge": "7",
},
},
},
},
wantErr: nil,
},
{
name: "Error: Invalid log level",
config: Config{
Level: "invalid",
Destinations: []destination{
{Type: Stdout},
},
},
wantErr: ErrInvalidLogLevel,
},
{
name: "Error: No destinations provided",
config: Config{
Level: InfoLevel,
Destinations: []destination{},
},
wantErr: ErrLogDestinationNil,
},
{
name: "Error: Invalid destination type",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{Type: "unknown"},
},
},
wantErr: fmt.Errorf("invalid destination type 'unknown'"),
},
{
name: "Error: Missing file path for file destination",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{
Type: File,
Config: map[string]string{
"maxSize": "10",
},
},
},
},
wantErr: ErrMissingFilePath,
},
{
name: "Error: Invalid maxSize value in file destination",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{
Type: File,
Config: map[string]string{
"path": "./logs/app.log",
"maxSize": "invalid",
},
},
},
},
wantErr: errors.New(`invalid maxSize: strconv.Atoi: parsing "invalid": invalid syntax`),
},
{
name: "Error: Invalid maxBackups value in file destination",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{
Type: File,
Config: map[string]string{
"path": "./logs/app.log",
"maxBackups": "invalid",
},
},
},
},
wantErr: errors.New(`invalid maxBackups: strconv.Atoi: parsing "invalid": invalid syntax`),
},
{
name: "Error: Invalid maxAge value in file destination",
config: Config{
Level: InfoLevel,
Destinations: []destination{
{
Type: File,
Config: map[string]string{
"path": "./logs/app.log",
"maxAge": "invalid",
},
},
},
},
wantErr: errors.New(`invalid maxAge: strconv.Atoi: parsing "invalid": invalid syntax`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.validate()
if (err == nil) != (tt.wantErr == nil) {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
}
if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() {
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}