Skip to main content
GORM includes a built-in structured logger that records SQL statements, execution time, row counts, and slow query warnings. You can configure it, silence it, or replace it entirely with your own implementation.

Log levels

Four log levels are defined as logger.LogLevel constants:
LevelConstantBehavior
1logger.SilentAll logging suppressed
2logger.ErrorOnly errors logged
3logger.WarnErrors and slow query warnings logged
4logger.InfoEvery SQL statement logged
Levels are ordered: Silent < Error < Warn < Info. Anything at or below the configured level is printed.

Default logger

When you do not supply a logger in gorm.Config, GORM uses logger.Default:
// Defined in gorm.io/gorm/logger
var Default = New(
    log.New(os.Stdout, "\r\n", log.LstdFlags),
    Config{
        SlowThreshold:             200 * time.Millisecond,
        LogLevel:                  Warn,
        IgnoreRecordNotFoundError: false,
        Colorful:                  true,
    },
)
The default logger writes to os.Stdout with ANSI color codes enabled and warns on queries that take longer than 200ms. A second built-in logger, logger.Discard, silently drops all log output:
var Discard = New(log.New(io.Discard, "", log.LstdFlags), Config{})

Changing the log level

Call LogMode to get a new logger instance at a different level. The original logger is not modified.
import (
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/driver/postgres"
)

// Silence all logging
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Silent),
})

// Log every SQL statement
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Info),
})

Debug shorthand

db.Debug() creates a new session with Info-level logging enabled for the current statement chain. It does not change the global logger.
// Only this query is logged at Info level
db.Debug().Find(&users)

// Equivalent to:
tx := db.Session(&gorm.Session{
    Logger: db.Logger.LogMode(logger.Info),
})
tx.Find(&users)

Customizing the default logger

Create a new logger with logger.New, supplying your own Writer and Config:
import (
    "log"
    "os"
    "time"
    "gorm.io/gorm/logger"
)

newLogger := logger.New(
    log.New(os.Stdout, "\r\n", log.LstdFlags), // writer
    logger.Config{
        SlowThreshold:             500 * time.Millisecond, // warn when query > 500ms
        LogLevel:                  logger.Info,            // log all SQL
        IgnoreRecordNotFoundError: true,                   // suppress not-found errors
        ParameterizedQueries:      false,                  // include parameter values
        Colorful:                  false,                  // disable ANSI colors
    },
)

db, err := gorm.Open(dialector, &gorm.Config{
    Logger: newLogger,
})

Logger config fields

SlowThreshold
time.Duration
default:"200ms"
Queries that exceed this duration are logged at Warn level with a SLOW SQL prefix. Set to 0 to disable slow query detection.
LogLevel
logger.LogLevel
default:"logger.Warn"
The minimum severity level that is emitted. One of Silent, Error, Warn, or Info.
IgnoreRecordNotFoundError
bool
default:"false"
When true, gorm.ErrRecordNotFound is not logged as an error. Useful in applications where a missing record is an expected, non-exceptional condition.
ParameterizedQueries
bool
default:"false"
When true, bind parameters are not interpolated into the logged SQL. The logged statement uses ? placeholders instead, which prevents sensitive values from appearing in logs.
// With ParameterizedQueries: false (default)
// Logs: SELECT * FROM users WHERE id = 42

// With ParameterizedQueries: true
// Logs: SELECT * FROM users WHERE id = ?
Colorful
bool
default:"true"
When true, ANSI escape codes are added to colorize log output. Disable when writing to a file or a structured log aggregator that does not render terminal colors.

Logger interface

Any type implementing logger.Interface can be used as a GORM logger:
type Interface interface {
    // LogMode returns a new logger instance at the given level
    LogMode(LogLevel) Interface

    // Info logs an informational message
    Info(context.Context, string, ...interface{})

    // Warn logs a warning message
    Warn(context.Context, string, ...interface{})

    // Error logs an error message
    Error(context.Context, string, ...interface{})

    // Trace logs a SQL statement with elapsed time and affected row count.
    // fc is called lazily to build the SQL string and row count only if the
    // log level requires it.
    Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}
The Trace method receives a lazy function fc rather than the SQL string directly. Call fc() only when you intend to log the statement to avoid unnecessary string formatting.

Implementing a custom logger

The example below routes all GORM log output to a zerolog instance:
import (
    "context"
    "time"
    "github.com/rs/zerolog"
    "gorm.io/gorm/logger"
)

type zerologGORMLogger struct {
    zl    zerolog.Logger
    level logger.LogLevel
    slowThreshold time.Duration
}

func (l *zerologGORMLogger) LogMode(level logger.LogLevel) logger.Interface {
    copy := *l
    copy.level = level
    return &copy
}

func (l *zerologGORMLogger) Info(ctx context.Context, msg string, data ...interface{}) {
    if l.level >= logger.Info {
        l.zl.Info().Msgf(msg, data...)
    }
}

func (l *zerologGORMLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
    if l.level >= logger.Warn {
        l.zl.Warn().Msgf(msg, data...)
    }
}

func (l *zerologGORMLogger) Error(ctx context.Context, msg string, data ...interface{}) {
    if l.level >= logger.Error {
        l.zl.Error().Msgf(msg, data...)
    }
}

func (l *zerologGORMLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
    if l.level <= logger.Silent {
        return
    }
    sql, rows := fc()
    elapsed := time.Since(begin)
    event := l.zl.Debug().
        Str("sql", sql).
        Int64("rows", rows).
        Dur("elapsed", elapsed)
    if err != nil {
        event = l.zl.Error().Err(err).Str("sql", sql)
    } else if l.slowThreshold != 0 && elapsed > l.slowThreshold {
        event = l.zl.Warn().Str("sql", sql).Dur("elapsed", elapsed)
    }
    event.Send()
}
Register it when opening the database:
db, err := gorm.Open(dialector, &gorm.Config{
    Logger: &zerologGORMLogger{
        zl:            zerolog.New(os.Stderr),
        level:         logger.Info,
        slowThreshold: 500 * time.Millisecond,
    },
})
Or scope it to a single session:
tx := db.Session(&gorm.Session{
    Logger: &zerologGORMLogger{...},
})

slog integration

For Go 1.21 and later, GORM ships a logger.NewSlogLogger constructor that wraps *slog.Logger:
import (
    "log/slog"
    "os"
    "time"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

slogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

gormLogger := logger.NewSlogLogger(slogger, logger.Config{
    SlowThreshold:             200 * time.Millisecond,
    LogLevel:                  logger.Info,
    IgnoreRecordNotFoundError: true,
    ParameterizedQueries:      false,
})

db, err := gorm.Open(dialector, &gorm.Config{
    Logger: gormLogger,
})
SQL trace events are logged as structured records with the following fields:
FieldTypeDescription
trace.durationstringElapsed time in milliseconds ("1.234ms")
trace.sqlstringThe SQL statement
trace.rowsint64Number of rows affected (omitted when -1)
trace.errorstringError message, only present on error events
Example JSON output:
{
  "time": "2024-01-15T10:00:00Z",
  "level": "INFO",
  "msg": "SQL executed",
  "trace": {
    "duration": "0.843ms",
    "sql": "SELECT * FROM users WHERE id = 42",
    "rows": 1
  }
}
The Colorful field in Config has no effect when using NewSlogLogger. Structured log handlers manage their own output formatting.

Build docs developers (and LLMs) love