GORM provides a flexible logging interface for tracking SQL queries, errors, and slow queries. The gorm.io/gorm/logger package includes a default logger implementation and interfaces for custom loggers.
import "gorm.io/gorm/logger"type Interface interface {
LogMode(LogLevel) Interface
Info(context.Context, string, ...interface{})
Warn(context.Context, string, ...interface{})
Error(context.Context, string, ...interface{})
Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}type LogLevel int
const (
Silent LogLevel = iota + 1 // No logging
Error // Log errors only
Warn // Log warnings and errors
Info // Log info, warnings, and errors
)Usage:
import "gorm.io/gorm/logger"
// Set log level globally
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
// Set log level for session
db.Session(&gorm.Session{
Logger: logger.Default.LogMode(logger.Silent),
})
// Enable debug mode (same as Info level)
db.Debug().Find(&users)type Config struct {
SlowThreshold time.Duration // Slow SQL threshold
Colorful bool // Enable colored output
IgnoreRecordNotFoundError bool // Don't log ErrRecordNotFound
ParameterizedQueries bool // Log parameterized queries
LogLevel LogLevel // Log level
}// Writer interface for logger output
type Writer interface {
Printf(string, ...interface{})
}
// Create new logger
func New(writer Writer, config Config) InterfaceUsage:
import (
"log"
"os"
"time"
"gorm.io/gorm/logger"
)
// Create custom logger
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound
Colorful: true, // Enable color
ParameterizedQueries: false, // Log with actual values
},
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})var (
// Default logger writes to stdout with colors
Default Interface
// Discard logger that ignores all logs
Discard Interface
// Recorder logger that records SQL for testing
Recorder Interface
)Usage:
import "gorm.io/gorm/logger"
// Use default logger
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default,
})
// Disable all logging
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Discard,
})
// Use recorder for testing
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Recorder,
})
// Get recorded SQL
sql := db.Statement.SQL.String()ANSI color codes for terminal output.
const (
Reset = "\033[0m"
Red = "\033[31m"
Green = "\033[32m"
Yellow = "\033[33m"
Blue = "\033[34m"
Magenta = "\033[35m"
Cyan = "\033[36m"
White = "\033[37m"
BlueBold = "\033[34;1m"
MagentaBold = "\033[35;1m"
RedBold = "\033[31;1m"
YellowBold = "\033[33;1m"
)Set the log level and return a new logger instance.
logger := logger.Default.LogMode(logger.Info)
db.Session(&gorm.Session{Logger: logger})Log informational messages.
logger.Info(ctx, "Custom info message: %s", "details")Log warning messages.
logger.Warn(ctx, "Custom warning: %s", "warning details")Log error messages.
logger.Error(ctx, "Custom error: %v", err)Log SQL execution trace (called automatically by GORM).
// Automatically called by GORM for each query
logger.Trace(ctx, begin, func() (string, int64) {
return sql, rowsAffected
}, err)Implement the logger interface for custom logging behavior.
import (
"context"
"fmt"
"time"
"gorm.io/gorm/logger"
)
type CustomLogger struct {
LogLevel logger.LogLevel
}
func (l *CustomLogger) LogMode(level logger.LogLevel) logger.Interface {
newLogger := *l
newLogger.LogLevel = level
return &newLogger
}
func (l *CustomLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= logger.Info {
fmt.Printf("[INFO] "+msg+"\n", data...)
}
}
func (l *CustomLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= logger.Warn {
fmt.Printf("[WARN] "+msg+"\n", data...)
}
}
func (l *CustomLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= logger.Error {
fmt.Printf("[ERROR] "+msg+"\n", data...)
}
}
func (l *CustomLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.LogLevel <= logger.Silent {
return
}
elapsed := time.Since(begin)
sql, rows := fc()
if err != nil && l.LogLevel >= logger.Error {
fmt.Printf("[ERROR] SQL: %s, Duration: %v, Error: %v\n", sql, elapsed, err)
} else if l.LogLevel >= logger.Info {
fmt.Printf("[SQL] %s, Duration: %v, Rows: %d\n", sql, elapsed, rows)
}
}
// Use custom logger
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: &CustomLogger{LogLevel: logger.Info},
})// Enable logging for specific query
db.Debug().Where("age > ?", 18).Find(&users)
// Custom logger for specific session
customLogger := logger.Default.LogMode(logger.Info)
db.Session(&gorm.Session{
Logger: customLogger,
}).Find(&users)
// Silent mode for specific query
db.Session(&gorm.Session{
Logger: logger.Default.LogMode(logger.Silent),
}).Create(&user)logLevel := logger.Silent
if os.Getenv("DEBUG") == "true" {
logLevel = logger.Info
}
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
})Configure threshold for slow query warnings.
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond, // Log queries slower than 200ms
LogLevel: logger.Warn, // Only log slow queries
},
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})Control whether to log parameterized queries or queries with actual values.
// Log with actual values (default)
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
ParameterizedQueries: false,
},
)
// Output: SELECT * FROM users WHERE age = 18
// Log with placeholders
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
ParameterizedQueries: true,
},
)
// Output: SELECT * FROM users WHERE age = ?newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
IgnoreRecordNotFoundError: true, // Don't log ErrRecordNotFound
LogLevel: logger.Error,
},
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: newLogger,
})
// ErrRecordNotFound will not be logged
var user User
db.First(&user, 9999) // No error log if not foundPass context through queries for request tracking.
// Create context with request ID
ctx := context.WithValue(context.Background(), "request_id", "abc-123")
// Context-aware logger
type ContextLogger struct {
logger.Interface
}
func (l *ContextLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
requestID := ctx.Value("request_id")
sql, rows := fc()
elapsed := time.Since(begin)
fmt.Printf("[%v] SQL: %s, Duration: %v, Rows: %d\n",
requestID, sql, elapsed, rows)
// Call original Trace
l.Interface.Trace(ctx, begin, fc, err)
}
// Use with context
db.WithContext(ctx).Find(&users)Integrate with structured logging libraries.
import (
"context"
"time"
"github.com/sirupsen/logrus"
"gorm.io/gorm/logger"
)
type LogrusLogger struct {
Logger *logrus.Logger
LogLevel logger.LogLevel
}
func (l *LogrusLogger) LogMode(level logger.LogLevel) logger.Interface {
newLogger := *l
newLogger.LogLevel = level
return &newLogger
}
func (l *LogrusLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= logger.Info {
l.Logger.WithContext(ctx).Infof(msg, data...)
}
}
func (l *LogrusLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= logger.Warn {
l.Logger.WithContext(ctx).Warnf(msg, data...)
}
}
func (l *LogrusLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.LogLevel >= logger.Error {
l.Logger.WithContext(ctx).Errorf(msg, data...)
}
}
func (l *LogrusLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.LogLevel <= logger.Silent {
return
}
elapsed := time.Since(begin)
sql, rows := fc()
fields := logrus.Fields{
"sql": sql,
"duration": elapsed,
"rows": rows,
}
if err != nil {
fields["error"] = err
l.Logger.WithContext(ctx).WithFields(fields).Error("SQL Error")
} else {
l.Logger.WithContext(ctx).WithFields(fields).Info("SQL Trace")
}
}
// Use Logrus logger
logrusLogger := logrus.New()
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: &LogrusLogger{
Logger: logrusLogger,
LogLevel: logger.Info,
},
})Use the recorder logger for testing SQL generation.
import (
"testing"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/driver/sqlite"
)
func TestQuery(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Recorder,
})
if err != nil {
t.Fatal(err)
}
// Perform query
var users []User
db.Where("age > ?", 18).Find(&users)
// Check SQL was generated correctly
sql := db.Statement.SQL.String()
if !strings.Contains(sql, "WHERE age > ?") {
t.Errorf("Expected WHERE clause in SQL: %s", sql)
}
}// Use appropriate log level
productionLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 500 * time.Millisecond,
LogLevel: logger.Warn, // Only errors and slow queries
IgnoreRecordNotFoundError: true,
Colorful: false, // Disable colors in production
ParameterizedQueries: true, // Don't log sensitive data
},
)// Verbose logging for development
devLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Info, // Log all queries
IgnoreRecordNotFoundError: false,
Colorful: true, // Enable colors
ParameterizedQueries: false, // Show actual values
},
)Helper functions for logging and SQL debugging.
// ExplainSQL generates SQL string with parameters interpolated for debugging
// WARNING: Do NOT execute the returned SQL - it's for logging only and may have SQL injection vulnerabilities
func ExplainSQL(sql string, numericPlaceholder *regexp.Regexp, escaper string, avars ...interface{}) string
// NewSlogLogger creates a new logger using Go's structured logging (log/slog)
func NewSlogLogger(logger *slog.Logger, config Config) Interface
// RecorderParamsFilter is a params filter for SQL recorder
var RecorderParamsFilter func(ctx context.Context, sql string, params ...interface{}) (string, []interface{})Usage:
import (
"log/slog"
"os"
"regexp"
"gorm.io/gorm/logger"
)
// Use slog-based logger
slogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.NewSlogLogger(slogger, logger.Config{
LogLevel: logger.Info,
SlowThreshold: 200 * time.Millisecond,
IgnoreRecordNotFoundError: true,
}),
})
// Use ExplainSQL for debugging
sql := "SELECT * FROM users WHERE id = ? AND name = ?"
params := []interface{}{1, "Alice"}
explainedSQL := logger.ExplainSQL(sql, nil, "'", params...)
fmt.Println(explainedSQL) // SELECT * FROM users WHERE id = 1 AND name = 'Alice'// Filter sensitive data from logs
type FilteredLogger struct {
logger.Interface
}
func (l *FilteredLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
sql, rows := fc()
// Filter sensitive data
sql = strings.ReplaceAll(sql, "password", "***")
sql = strings.ReplaceAll(sql, "token", "***")
// Log filtered SQL
elapsed := time.Since(begin)
fmt.Printf("SQL: %s, Duration: %v, Rows: %d\n", sql, elapsed, rows)
}