From 9c4587a22a1ca210d0470c26a181781d7e9d7001 Mon Sep 17 00:00:00 2001 From: sundayonah Date: Tue, 18 Mar 2025 16:19:15 +0100 Subject: [PATCH 1/4] feat: improve Sentry error logging with enhanced context and stack traces --- go.mod | 4 +- go.sum | 15 ++-- utils/logger/logger.go | 156 ++++++++++++++++++++++++++---------- utils/logger/logger_test.go | 152 +++++++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+), 50 deletions(-) create mode 100644 utils/logger/logger_test.go diff --git a/go.mod b/go.mod index 730abaca..aa6db5a0 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,12 @@ require ( github.com/JGLTechnologies/gin-rate-limit v1.5.4 github.com/anaskhan96/base58check v0.0.0-20181220122047-b05365d494c4 github.com/btcsuite/btcd/btcec/v2 v2.3.2 - github.com/chadsr/logrus-sentry v0.4.1 - github.com/getsentry/sentry-go v0.13.0 + github.com/getsentry/sentry-go v0.31.1 github.com/gin-gonic/gin v1.9.1 github.com/go-co-op/gocron v1.35.0 github.com/golang-jwt/jwt/v5 v5.0.0 github.com/jarcoal/httpmock v1.3.1 + github.com/joho/godotenv v1.5.1 github.com/mailgun/mailgun-go/v3 v3.6.4 github.com/mattn/go-sqlite3 v1.14.16 github.com/opus-domini/fast-shot v0.10.0 diff --git a/go.sum b/go.sum index acdd581a..3cf41fd1 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,6 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chadsr/logrus-sentry v0.4.1 h1:WC9UQPdWBpIB7bAwtVRsKAhj2Mn8t3XjmZxvZHrHWbo= -github.com/chadsr/logrus-sentry v0.4.1/go.mod h1:9LQsk92Gb2H2+PyCSoJoKBdMhqVIRO/5Eo3Muor4bq4= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -212,8 +210,8 @@ github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= -github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= -github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= +github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= +github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -225,8 +223,9 @@ github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuY github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-co-op/gocron v1.35.0 h1:niC91OHiSEimXgPPay02AI1gLGL4JGBgDzmWtgZ8n5A= github.com/go-co-op/gocron v1.35.0/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -386,6 +385,8 @@ github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+ github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -566,8 +567,6 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -659,6 +658,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/utils/logger/logger.go b/utils/logger/logger.go index 56872657..a4538a99 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -2,12 +2,13 @@ package logger import ( "bytes" + "fmt" + "io" "os" "path/filepath" "strings" "time" - sentryhook "github.com/chadsr/logrus-sentry" "github.com/getsentry/sentry-go" "github.com/paycrest/aggregator/config" "github.com/sirupsen/logrus" @@ -16,43 +17,31 @@ import ( var logger = logrus.New() func init() { + logger.Level = logrus.InfoLevel logger.Formatter = &formatter{} + cfg := config.ServerConfig() - config := config.ServerConfig() - - if config.Environment == "production" || config.Environment == "staging" { - // init sentry + if cfg.Environment == "production" || cfg.Environment == "staging" { err := sentry.Init(sentry.ClientOptions{ - Dsn: config.SentryDSN, + Dsn: cfg.SentryDSN, + Environment: cfg.Environment, + AttachStacktrace: true, + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + return event + }, }) if err != nil { logger.Fatalf("Sentry initialization failed: %v", err) } - - // Sentry hook - hook := sentryhook.New([]logrus.Level{ - logrus.PanicLevel, - logrus.FatalLevel, - logrus.ErrorLevel, - }) - logger.Hooks.Add(hook) } else { - // File hook for local environment - - // Get the directory of the executable ex, err := os.Executable() if err != nil { logger.Errorf("Failed to get the executable path: %v", err) return } - - // Get the directory of the executable exDir := filepath.Dir(ex) - - // Construct the file path in the same directory as the executable filePath := filepath.Join(exDir, "logs.txt") - file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err == nil { logger.Out = file @@ -63,6 +52,39 @@ func init() { logger.SetReportCaller(true) } +// InitForTest initializes the logger with custom config and executable path for testing +func InitForTest(cfg config.ServerConfiguration, output io.Writer, executablePath string) { + logger.Level = logrus.InfoLevel + logger.Formatter = &formatter{} + logger.Out = output + + if cfg.Environment == "production" || cfg.Environment == "staging" { + err := sentry.Init(sentry.ClientOptions{ + Dsn: cfg.SentryDSN, + Environment: cfg.Environment, + AttachStacktrace: true, + BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { + return event + }, + }) + if err != nil { + logger.Fatalf("Sentry initialization failed: %v", err) + } + } else { + if executablePath != "" { + exDir := filepath.Dir(executablePath) + filePath := filepath.Join(exDir, "logs.txt") + file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err == nil { + logger.Out = file + } else { + logger.Errorf("Failed to open logs.txt: %v", err) + } + } + } + logger.SetReportCaller(true) +} + // SetLogLevel sets the log level for the logger. func SetLogLevel(level logrus.Level) { logger.Level = level @@ -71,55 +93,98 @@ func SetLogLevel(level logrus.Level) { // Fields type, used to pass to `WithFields`. type Fields logrus.Fields -// Debugf logs a message at level Debug on the standard logger. -func Debugf(format string, args ...interface{}) { +// ErrorWithFields logs an error with additional context +func ErrorWithFields(err error, fields Fields) { + if logger.Level >= logrus.ErrorLevel { + wrappedErr := fmt.Errorf("error occurred: %w", err) // Add context + sentry.WithScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelError) // Explicitly set level + for key, value := range fields { + switch v := value.(type) { + case string: + scope.SetTag(key, v) + default: + scope.SetExtra(key, value) + } + } + sentry.CaptureException(wrappedErr) + }) + logger.WithFields(logrus.Fields(fields)).Error(wrappedErr.Error()) + } +} + +// Debugf logs a message at level Debug with optional fields +func Debugf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.DebugLevel { - entry := logger.WithFields(logrus.Fields{}) + entry := logger.WithFields(logrus.Fields(fields)) entry.Debugf(format, args...) } } -// Infof logs a message at level Info on the standard logger. -func Infof(format string, args ...interface{}) { +// Infof logs a message at level Info with optional fields +func Infof(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.InfoLevel { - entry := logger.WithFields(logrus.Fields{}) + entry := logger.WithFields(logrus.Fields(fields)) entry.Infof(format, args...) } } -// Warnf logs a message at level Warn on the standard logger. -func Warnf(format string, args ...interface{}) { +// Warnf logs a message at level Warn with optional fields +func Warnf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.WarnLevel { - entry := logger.WithFields(logrus.Fields{}) + sentry.WithScope(func(scope *sentry.Scope) { + for key, value := range fields { + scope.SetExtra(key, value) + } + sentry.CaptureMessage(fmt.Sprintf(format, args...)) + }) + entry := logger.WithFields(logrus.Fields(fields)) entry.Warnf(format, args...) } } -// Errorf logs a message at level Error on the standard logger. -func Errorf(format string, args ...interface{}) { +// Errorf logs an error message with fields and stack trace +func Errorf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.ErrorLevel { - entry := logger.WithFields(logrus.Fields{}) - entry.Errorf(format, args...) + errMsg := fmt.Sprintf(format, args...) + sentry.WithScope(func(scope *sentry.Scope) { + for key, value := range fields { + switch v := value.(type) { + case string: + scope.SetTag(key, v) + default: + scope.SetExtra(key, value) + } + } + sentry.CaptureMessage(errMsg) + }) + logger.WithFields(logrus.Fields(fields)).Error(errMsg) } } -// Fatalf logs a message at level Fatal on the standard logger. -func Fatalf(format string, args ...interface{}) { +// Fatalf logs a fatal message with fields +func Fatalf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.FatalLevel { - entry := logger.WithFields(logrus.Fields{}) + errMsg := fmt.Sprintf(format, args...) + sentry.WithScope(func(scope *sentry.Scope) { + for key, value := range fields { + scope.SetExtra(key, value) + } + sentry.CaptureMessage(errMsg) + }) + entry := logger.WithFields(logrus.Fields(fields)) entry.Fatalf(format, args...) } } -// Formatter implements logrus.Formatter interface. +// Formatter implements logrus.Formatter interface type formatter struct { prefix string } -// Format building log message. +// Format building log message func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { var sb bytes.Buffer - sb.WriteString(strings.ToUpper(entry.Level.String())) sb.WriteString(" ") sb.WriteString(entry.Time.Format(time.RFC3339)) @@ -127,5 +192,14 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { sb.WriteString(f.prefix) sb.WriteString(entry.Message) + if len(entry.Data) > 0 { + sb.WriteString(" [") + for key, value := range entry.Data { + sb.WriteString(fmt.Sprintf("%s=%v ", key, value)) + } + sb.WriteString("]") + } + sb.WriteString("\n") + return sb.Bytes(), nil } diff --git a/utils/logger/logger_test.go b/utils/logger/logger_test.go new file mode 100644 index 00000000..c4dec498 --- /dev/null +++ b/utils/logger/logger_test.go @@ -0,0 +1,152 @@ +package logger + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/getsentry/sentry-go" + "github.com/paycrest/aggregator/config" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +// MockConfig mocks the ServerConfig for testing +type MockConfig struct { + Environment string + SentryDSN string +} + +func (m MockConfig) ServerConfig() config.ServerConfiguration { + return config.ServerConfiguration{ + Environment: m.Environment, + SentryDSN: m.SentryDSN, + } +} + +func TestLoggerComprehensive(t *testing.T) { + conf := config.ServerConfig() + if conf.SentryDSN == "" { + t.Fatal("SENTRY_DSN not set in config.ServerConfig(), cannot send to Sentry") + } + + tempDir := t.TempDir() + var buf bytes.Buffer + defer sentry.Flush(2 * time.Second) + + reinitLogger := func(env string, executablePath string) { + mockCfg := MockConfig{ + Environment: env, + SentryDSN: conf.SentryDSN, + } + InitForTest(mockCfg.ServerConfig(), &buf, executablePath) + } + + tests := []struct { + name string + env string + executablePath string + action func(t *testing.T) + verify func(t *testing.T, output string, fileContent string) + }{ + { + name: "Production with ErrorWithFields", + env: "production", + executablePath: "", + action: func(t *testing.T) { + reinitLogger("production", "") + testErr := errors.New("test error from go test") + fields := Fields{ + "order_id": "123", + "user_id": 456, + "test": "true", + } + t.Log("Sending wrapped error to Sentry") + ErrorWithFields(testErr, fields) + time.Sleep(100 * time.Millisecond) + }, + verify: func(t *testing.T, output string, fileContent string) { + assert.Contains(t, output, "ERROR", "Should contain error level") + assert.Contains(t, output, "error occurred: test error from go test", "Should contain wrapped error message") + assert.Contains(t, output, "order_id=123", "Should contain field order_id") + assert.Contains(t, output, "user_id=456", "Should contain field user_id") + assert.Empty(t, fileContent, "No file output in production") + t.Log("Check Sentry for event: 'error occurred: test error from go test' with tags order_id=123, test=true, extra user_id=456, level=error") + }, + }, + { + name: "Development with Infof", + env: "development", + executablePath: filepath.Join(tempDir, "test"), + action: func(t *testing.T) { + reinitLogger("development", filepath.Join(tempDir, "test")) + Infof("Test info %s", Fields{"key": "value"}, "message") + }, + verify: func(t *testing.T, output string, fileContent string) { + assert.Empty(t, output, "Output should go to file in development") + assert.Contains(t, fileContent, "INFO", "Should contain info level") + assert.Contains(t, fileContent, "Test info message", "Should contain formatted message") + assert.Contains(t, fileContent, "key=value", "Should contain field") + }, + }, + { + name: "Production with Errorf", + env: "production", + executablePath: "", + action: func(t *testing.T) { + reinitLogger("production", "") + Errorf("Payment failed %d", Fields{"amount": 99}, 1) + time.Sleep(100 * time.Millisecond) + }, + verify: func(t *testing.T, output string, fileContent string) { + assert.Contains(t, output, "ERROR", "Should contain error level") + assert.Contains(t, output, "Payment failed 1", "Should contain formatted message") + assert.Contains(t, output, "amount=99", "Should contain field") + t.Log("Check Sentry for event: 'Payment failed 1' with extra amount=99") + }, + }, + { + name: "Level Filtering in Development", + env: "development", + executablePath: filepath.Join(tempDir, "test"), + action: func(t *testing.T) { + reinitLogger("development", filepath.Join(tempDir, "test")) + SetLogLevel(logrus.WarnLevel) + Debugf("Debug %s", Fields{}, "test") + Infof("Info %s", Fields{}, "test") + Warnf("Warn %s", Fields{}, "test") + }, + verify: func(t *testing.T, output string, fileContent string) { + assert.NotContains(t, fileContent, "DEBUG", "Debug should be filtered") + assert.NotContains(t, fileContent, "INFO", "Info should be filtered") + assert.Contains(t, fileContent, "WARN", "Warn should appear") + assert.Contains(t, fileContent, "Warn test", "Warn message should appear") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf.Reset() + tt.action(t) + fileContent := "" + if tt.env != "production" && tt.env != "staging" { + data, err := os.ReadFile(filepath.Join(tempDir, "logs.txt")) + assert.NoError(t, err) + fileContent = string(data) + err = os.Truncate(filepath.Join(tempDir, "logs.txt"), 0) + assert.NoError(t, err) + } + tt.verify(t, buf.String(), fileContent) + }) + } +} + +func TestMain(m *testing.M) { + result := m.Run() + sentry.Flush(2 * time.Second) + os.Exit(result) +} From 6259338db37298634459b3434de056ece748b07c Mon Sep 17 00:00:00 2001 From: sundayonah Date: Tue, 18 Mar 2025 17:35:07 +0100 Subject: [PATCH 2/4] refactor(logger): optimize initialization and remove duplication --- utils/logger/logger.go | 63 ++++++++++++------------------------- utils/logger/logger_test.go | 4 +-- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/utils/logger/logger.go b/utils/logger/logger.go index a4538a99..681f9917 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -16,31 +16,24 @@ import ( var logger = logrus.New() -func init() { - +func initializeLogger(cfg *config.ServerConfiguration, output io.Writer, executablePath string) { logger.Level = logrus.InfoLevel logger.Formatter = &formatter{} - cfg := config.ServerConfig() + logger.Out = output if cfg.Environment == "production" || cfg.Environment == "staging" { - err := sentry.Init(sentry.ClientOptions{ + if err := sentry.Init(sentry.ClientOptions{ Dsn: cfg.SentryDSN, Environment: cfg.Environment, AttachStacktrace: true, BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { return event }, - }) - if err != nil { + }); err != nil { logger.Fatalf("Sentry initialization failed: %v", err) } - } else { - ex, err := os.Executable() - if err != nil { - logger.Errorf("Failed to get the executable path: %v", err) - return - } - exDir := filepath.Dir(ex) + } else if executablePath != "" { + exDir := filepath.Dir(executablePath) filePath := filepath.Join(exDir, "logs.txt") file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err == nil { @@ -49,40 +42,24 @@ func init() { logger.Errorf("Failed to open logs.txt: %v", err) } } + logger.SetReportCaller(true) } -// InitForTest initializes the logger with custom config and executable path for testing -func InitForTest(cfg config.ServerConfiguration, output io.Writer, executablePath string) { - logger.Level = logrus.InfoLevel - logger.Formatter = &formatter{} - logger.Out = output - - if cfg.Environment == "production" || cfg.Environment == "staging" { - err := sentry.Init(sentry.ClientOptions{ - Dsn: cfg.SentryDSN, - Environment: cfg.Environment, - AttachStacktrace: true, - BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { - return event - }, - }) - if err != nil { - logger.Fatalf("Sentry initialization failed: %v", err) - } - } else { - if executablePath != "" { - exDir := filepath.Dir(executablePath) - filePath := filepath.Join(exDir, "logs.txt") - file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err == nil { - logger.Out = file - } else { - logger.Errorf("Failed to open logs.txt: %v", err) - } - } +func init() { + cfg := config.ServerConfig() + ex, err := os.Executable() + if err != nil { + logger.Errorf("Failed to get the executable path: %v", err) + return } - logger.SetReportCaller(true) + + initializeLogger(cfg, logger.Out, ex) +} + +// InitForTest initializes the logger with custom config and output for testing +func InitForTest(cfg *config.ServerConfiguration, output io.Writer, executablePath string) { + initializeLogger(cfg, output, executablePath) } // SetLogLevel sets the log level for the logger. diff --git a/utils/logger/logger_test.go b/utils/logger/logger_test.go index c4dec498..f2f3b401 100644 --- a/utils/logger/logger_test.go +++ b/utils/logger/logger_test.go @@ -20,8 +20,8 @@ type MockConfig struct { SentryDSN string } -func (m MockConfig) ServerConfig() config.ServerConfiguration { - return config.ServerConfiguration{ +func (m MockConfig) ServerConfig() *config.ServerConfiguration { + return &config.ServerConfiguration{ Environment: m.Environment, SentryDSN: m.SentryDSN, } From c4b9aba557674a5cd8d19725f48b79b4c3aca9b6 Mon Sep 17 00:00:00 2001 From: sundayonah Date: Tue, 18 Mar 2025 19:09:49 +0100 Subject: [PATCH 3/4] fix(logger): Ensure development logs go to file and improve Sentry error formatting with more test cases --- utils/logger/logger.go | 103 +++++++++++++++++++++++++----------- utils/logger/logger_test.go | 45 ++++++++++++---- 2 files changed, 108 insertions(+), 40 deletions(-) diff --git a/utils/logger/logger.go b/utils/logger/logger.go index 681f9917..7aa8fa5d 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -14,13 +14,19 @@ import ( "github.com/sirupsen/logrus" ) +// Logger instance var logger = logrus.New() -func initializeLogger(cfg *config.ServerConfiguration, output io.Writer, executablePath string) { +// InitLogger initializes the logger with the given configuration, output, and executable path. +func InitLogger(cfg *config.ServerConfiguration, output io.Writer, executablePath string) { + if cfg == nil { + cfg = config.ServerConfig() + } + logger.Level = logrus.InfoLevel logger.Formatter = &formatter{} - logger.Out = output + // Environment-specific configuration if cfg.Environment == "production" || cfg.Environment == "staging" { if err := sentry.Init(sentry.ClientOptions{ Dsn: cfg.SentryDSN, @@ -32,14 +38,41 @@ func initializeLogger(cfg *config.ServerConfiguration, output io.Writer, executa }); err != nil { logger.Fatalf("Sentry initialization failed: %v", err) } + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetTag("environment", cfg.Environment) + scope.SetExtra("app_version", "1.0.0") + }) + // Use provided output or default to stdout + if output == nil { + logger.Out = os.Stdout + } else { + logger.Out = output + } } else if executablePath != "" { + // In development, prioritize file output exDir := filepath.Dir(executablePath) + if err := os.MkdirAll(exDir, 0755); err != nil { + logger.Errorf("Failed to create directory %s: %v", exDir, err) + } filePath := filepath.Join(exDir, "logs.txt") file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err == nil { - logger.Out = file - } else { + if err != nil { logger.Errorf("Failed to open logs.txt: %v", err) + // Fallback to provided output or stdout + if output == nil { + logger.Out = os.Stdout + } else { + logger.Out = output + } + } else { + logger.Out = file // Default to file output in development + } + } else { + // Fallback for no executablePath + if output == nil { + logger.Out = os.Stdout + } else { + logger.Out = output } } @@ -51,15 +84,14 @@ func init() { ex, err := os.Executable() if err != nil { logger.Errorf("Failed to get the executable path: %v", err) - return + ex = "" } - - initializeLogger(cfg, logger.Out, ex) + InitLogger(cfg, nil, ex) } -// InitForTest initializes the logger with custom config and output for testing +// InitForTest is a wrapper for testing func InitForTest(cfg *config.ServerConfiguration, output io.Writer, executablePath string) { - initializeLogger(cfg, output, executablePath) + InitLogger(cfg, output, executablePath) } // SetLogLevel sets the log level for the logger. @@ -73,9 +105,9 @@ type Fields logrus.Fields // ErrorWithFields logs an error with additional context func ErrorWithFields(err error, fields Fields) { if logger.Level >= logrus.ErrorLevel { - wrappedErr := fmt.Errorf("error occurred: %w", err) // Add context + wrappedErr := fmt.Errorf("error occurred: %w", err) sentry.WithScope(func(scope *sentry.Scope) { - scope.SetLevel(sentry.LevelError) // Explicitly set level + scope.SetLevel(sentry.LevelError) for key, value := range fields { switch v := value.(type) { case string: @@ -86,45 +118,51 @@ func ErrorWithFields(err error, fields Fields) { } sentry.CaptureException(wrappedErr) }) - logger.WithFields(logrus.Fields(fields)).Error(wrappedErr.Error()) + logger.WithFields(logrus.Fields(fields)).Error(wrappedErr) } } // Debugf logs a message at level Debug with optional fields func Debugf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.DebugLevel { - entry := logger.WithFields(logrus.Fields(fields)) - entry.Debugf(format, args...) + logger.WithFields(logrus.Fields(fields)).Debugf(format, args...) } } // Infof logs a message at level Info with optional fields func Infof(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.InfoLevel { - entry := logger.WithFields(logrus.Fields(fields)) - entry.Infof(format, args...) + logger.WithFields(logrus.Fields(fields)).Infof(format, args...) } } // Warnf logs a message at level Warn with optional fields func Warnf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.WarnLevel { + wrappedErr := fmt.Errorf(format, args...) // Create error for stack trace sentry.WithScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelWarning) for key, value := range fields { - scope.SetExtra(key, value) + switch v := value.(type) { + case string: + scope.SetTag(key, v) + default: + scope.SetExtra(key, value) + } } - sentry.CaptureMessage(fmt.Sprintf(format, args...)) + sentry.CaptureException(wrappedErr) }) - entry := logger.WithFields(logrus.Fields(fields)) - entry.Warnf(format, args...) + logger.WithFields(logrus.Fields(fields)).Warnf(format, args...) } } // Errorf logs an error message with fields and stack trace func Errorf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.ErrorLevel { - errMsg := fmt.Sprintf(format, args...) + // Create the error directly with fmt.Errorf, avoiding intermediate fmt.Sprintf + wrappedErr := fmt.Errorf(format, args...) sentry.WithScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelError) for key, value := range fields { switch v := value.(type) { case string: @@ -133,24 +171,30 @@ func Errorf(format string, fields Fields, args ...interface{}) { scope.SetExtra(key, value) } } - sentry.CaptureMessage(errMsg) + sentry.CaptureException(wrappedErr) // Capture the error with stack trace }) - logger.WithFields(logrus.Fields(fields)).Error(errMsg) + // Log the formatted message directly + logger.WithFields(logrus.Fields(fields)).Errorf(format, args...) } } // Fatalf logs a fatal message with fields func Fatalf(format string, fields Fields, args ...interface{}) { if logger.Level >= logrus.FatalLevel { - errMsg := fmt.Sprintf(format, args...) + wrappedErr := fmt.Errorf(format, args...) sentry.WithScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelFatal) for key, value := range fields { - scope.SetExtra(key, value) + switch v := value.(type) { + case string: + scope.SetTag(key, v) + default: + scope.SetExtra(key, value) + } } - sentry.CaptureMessage(errMsg) + sentry.CaptureException(wrappedErr) }) - entry := logger.WithFields(logrus.Fields(fields)) - entry.Fatalf(format, args...) + logger.WithFields(logrus.Fields(fields)).Fatal(wrappedErr) } } @@ -177,6 +221,5 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { sb.WriteString("]") } sb.WriteString("\n") - return sb.Bytes(), nil } diff --git a/utils/logger/logger_test.go b/utils/logger/logger_test.go index f2f3b401..76610bb1 100644 --- a/utils/logger/logger_test.go +++ b/utils/logger/logger_test.go @@ -35,9 +35,9 @@ func TestLoggerComprehensive(t *testing.T) { tempDir := t.TempDir() var buf bytes.Buffer - defer sentry.Flush(2 * time.Second) reinitLogger := func(env string, executablePath string) { + buf.Reset() mockCfg := MockConfig{ Environment: env, SentryDSN: conf.SentryDSN, @@ -62,7 +62,7 @@ func TestLoggerComprehensive(t *testing.T) { fields := Fields{ "order_id": "123", "user_id": 456, - "test": "true", + "test": true, } t.Log("Sending wrapped error to Sentry") ErrorWithFields(testErr, fields) @@ -74,7 +74,7 @@ func TestLoggerComprehensive(t *testing.T) { assert.Contains(t, output, "order_id=123", "Should contain field order_id") assert.Contains(t, output, "user_id=456", "Should contain field user_id") assert.Empty(t, fileContent, "No file output in production") - t.Log("Check Sentry for event: 'error occurred: test error from go test' with tags order_id=123, test=true, extra user_id=456, level=error") + t.Log("Check Sentry for event: 'error occurred: test error from go test' with tags order_id=123, extra user_id=456, test=true, level=error") }, }, { @@ -84,6 +84,7 @@ func TestLoggerComprehensive(t *testing.T) { action: func(t *testing.T) { reinitLogger("development", filepath.Join(tempDir, "test")) Infof("Test info %s", Fields{"key": "value"}, "message") + time.Sleep(100 * time.Millisecond) // Allow file write }, verify: func(t *testing.T, output string, fileContent string) { assert.Empty(t, output, "Output should go to file in development") @@ -105,7 +106,8 @@ func TestLoggerComprehensive(t *testing.T) { assert.Contains(t, output, "ERROR", "Should contain error level") assert.Contains(t, output, "Payment failed 1", "Should contain formatted message") assert.Contains(t, output, "amount=99", "Should contain field") - t.Log("Check Sentry for event: 'Payment failed 1' with extra amount=99") + assert.Empty(t, fileContent, "No file output in production") + t.Log("Check Sentry for event: 'Payment failed 1' with extra amount=99, level=error") }, }, { @@ -118,27 +120,50 @@ func TestLoggerComprehensive(t *testing.T) { Debugf("Debug %s", Fields{}, "test") Infof("Info %s", Fields{}, "test") Warnf("Warn %s", Fields{}, "test") + time.Sleep(100 * time.Millisecond) // Allow file write }, verify: func(t *testing.T, output string, fileContent string) { + assert.Empty(t, output, "Output should go to file in development") assert.NotContains(t, fileContent, "DEBUG", "Debug should be filtered") assert.NotContains(t, fileContent, "INFO", "Info should be filtered") assert.Contains(t, fileContent, "WARN", "Warn should appear") assert.Contains(t, fileContent, "Warn test", "Warn message should appear") }, }, + { + name: "Staging with Warnf", + env: "staging", + executablePath: "", + action: func(t *testing.T) { + reinitLogger("staging", "") + Warnf("Warning %s", Fields{"reason": "test"}, "condition") + time.Sleep(100 * time.Millisecond) + }, + verify: func(t *testing.T, output string, fileContent string) { + assert.Contains(t, output, "WARN", "Should contain warn level") + assert.Contains(t, output, "Warning condition", "Should contain formatted message") + assert.Contains(t, output, "reason=test", "Should contain field") + assert.Empty(t, fileContent, "No file output in staging") + t.Log("Check Sentry for event: 'Warning condition' with extra reason=test, level=warning") + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - buf.Reset() tt.action(t) fileContent := "" if tt.env != "production" && tt.env != "staging" { - data, err := os.ReadFile(filepath.Join(tempDir, "logs.txt")) - assert.NoError(t, err) - fileContent = string(data) - err = os.Truncate(filepath.Join(tempDir, "logs.txt"), 0) - assert.NoError(t, err) + filePath := filepath.Join(tempDir, "logs.txt") + data, err := os.ReadFile(filePath) + if err != nil { + t.Errorf("Failed to read logs.txt: %v", err) + } else { + fileContent = string(data) + } + if err := os.Truncate(filePath, 0); err != nil { + t.Errorf("Failed to truncate logs.txt: %v", err) + } } tt.verify(t, buf.String(), fileContent) }) From 5d49a7e379dfe53493e32924b911eecf36cbc376 Mon Sep 17 00:00:00 2001 From: sundayonah Date: Mon, 21 Apr 2025 21:09:02 +0100 Subject: [PATCH 4/4] fix: Update logger.Errorf call to include required Fields parameter --- utils/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/utils.go b/utils/utils.go index 3954dcd7..6b34517d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -516,7 +516,7 @@ func GetTokenRateFromQueue(tokenSymbol string, orderAmount decimal.Decimal, fiat } parts := strings.Split(providerData, ":") if len(parts) != 5 { - logger.Errorf("utils.GetTokenRateFromQueue.InvalidProviderData: %v", providerData) + logger.Errorf("utils.GetTokenRateFromQueue.InvalidProviderData: %v", logger.Fields{"providerData": providerData}) continue }