Added Log package
Ft/logging module Merge pull request #426 from MayurWitsLab/ft/logging_module
This commit is contained in:
13
go.mod
13
go.mod
@@ -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
16
go.sum
@@ -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
271
pkg/log/log.go
Normal 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
667
pkg/log/log_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user