diff --git a/backend/internal/app/database.go b/backend/internal/app/database.go index fc04354e..431d3e8b 100644 --- a/backend/internal/app/database.go +++ b/backend/internal/app/database.go @@ -2,6 +2,8 @@ package app import ( "fmt" + "log" + "os" "time" "gorm.io/driver/postgres" @@ -16,7 +18,7 @@ import ( // Pattern follows Chatwoot's ActiveRecord PostgreSQL setup in config/database.yml func NewDatabase(cfg *config.DatabaseConfig, logLevel string) (*gorm.DB, error) { dsn := cfg.DSN - + // Map log level to GORM logger level var gormLogLevel gormlogger.LogLevel switch logLevel { @@ -33,8 +35,8 @@ func NewDatabase(cfg *config.DatabaseConfig, logLevel string) (*gorm.DB, error) } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - Logger: gormlogger.Default.LogMode(gormLogLevel), - PrepareStmt: true, + Logger: newDatabaseLogger(gormLogLevel), + PrepareStmt: true, DisableForeignKeyConstraintWhenMigrating: true, // migrate first, add FK constraints later }) if err != nil { @@ -58,3 +60,12 @@ func NewDatabase(cfg *config.DatabaseConfig, logLevel string) (*gorm.DB, error) return db, nil } + +func newDatabaseLogger(level gormlogger.LogLevel) gormlogger.Interface { + return gormlogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gormlogger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: level, + IgnoreRecordNotFoundError: true, + ParameterizedQueries: true, + }) +} diff --git a/backend/internal/app/database_test.go b/backend/internal/app/database_test.go new file mode 100644 index 00000000..7ded0d82 --- /dev/null +++ b/backend/internal/app/database_test.go @@ -0,0 +1,19 @@ +package app + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" +) + +func TestDatabaseLoggerRedactsQueryParameters(t *testing.T) { + filter, ok := newDatabaseLogger(gormlogger.Info).(gorm.ParamsFilter) + require.True(t, ok) + + sql, params := filter.ParamsFilter(context.Background(), "SELECT * FROM contact_inboxes WHERE pubsub_token = ?", "secret-token") + require.Equal(t, "SELECT * FROM contact_inboxes WHERE pubsub_token = ?", sql) + require.Empty(t, params) +} diff --git a/backend/internal/handler/ws/handler.go b/backend/internal/handler/ws/handler.go index 1e58252f..9767b399 100644 --- a/backend/internal/handler/ws/handler.go +++ b/backend/internal/handler/ws/handler.go @@ -66,7 +66,7 @@ func (h *Handler) ServeWS(c *gin.Context) { // Step 1: Authenticate (JWT or pubsub_token) claims, err := h.authenticator.Authenticate(c) if err != nil { - logger.L().Errorf("ws: authentication failed: %v", err) + logger.L().Warnf("ws: authentication rejected: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } diff --git a/backend/internal/ws/auth.go b/backend/internal/ws/auth.go index 47558941..e56df357 100644 --- a/backend/internal/ws/auth.go +++ b/backend/internal/ws/auth.go @@ -209,7 +209,7 @@ func (a *WSAuthenticator) AuthenticateAndServeWS(c *gin.Context) { // Step 1: Authenticate claims, err := a.Authenticate(c) if err != nil { - logger.L().Errorf("ws: authentication failed: %v", err) + logger.L().Warnf("ws: authentication rejected: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return }