Skip to content

Statement logging & debugging

LiteORM logs every executed SQL statement so that during development you can watch the queries go by and trace each one back to the line of Go that issued it. Each log event carries the SQL, the bind arguments, how long it took, the rows affected (for writes), the originating source location, and the error if any.

Logging goes through a standard *slog.Logger, so you pick the output: a colored, human-readable handler for development, or any structured handler (JSON, text, or an OpenTelemetry bridge) for production. Statement events are emitted at slog.LevelDebug, so logging is silent unless the logger is enabled for debug — there is no overhead in production where the level is higher.

The liteorm.org/log package is a ready-made handler tuned for SQL: one line per statement, colored by speed (green) / slow (yellow) / error (red), with the SQL, args, rows, and the file:line that issued it.

import (
liteorm "liteorm.org"
"liteorm.org/dialect/sqlite"
devlog "liteorm.org/log"
)
db, _ := sqlite.Open("app.db", liteorm.WithLogger(devlog.New(os.Stderr, nil)))

Output looks like:

15:04:05.123 [liteorm] 35µs INSERT INTO "widgets" ("name","price") VALUES (?, ?) RETURNING "id" args=["Gear", 500] (examples/blog/main.go:58)
15:04:05.123 [liteorm] 14µs SELECT "widgets"."id", "widgets"."name" FROM "widgets" WHERE "price" > ? args=[300] (examples/blog/main.go:60)

The (examples/blog/main.go:58) is the exact line in your code that ran the statement, shown relative to the working directory so it’s short and clickable. Each bind value is delimited and strings are quoted (args=["Gear", 500]), so a value that contains spaces is never ambiguous. Configure the handler with devlog.Options:

devlog.New(os.Stderr, &devlog.Options{
Color: true, // ANSI color (default true; forced off when NO_COLOR is set)
SlowThreshold: 200 * time.Millisecond, // statements at/over this are highlighted yellow
Level: slog.LevelDebug, // the floor; raise it to quieten
AbsPath: false, // true prints the absolute caller path instead of the working-dir-relative one
})

Color is on by default and disabled automatically when the NO_COLOR environment variable is set. The caller path is relative by default; set AbsPath: true to print the absolute path (the structured caller attribute below is always absolute).

For machine-readable logs, pass any standard slog handler — LiteORM emits the same events through it:

jsonLog := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
db, _ := sqlite.Open("app.db", liteorm.WithLogger(jsonLog))
{"level":"DEBUG","msg":"liteorm.query","sql":"SELECT count(*) FROM widgets","dur":16958,"args":null,"caller":"/home/me/app/main.go:75"}

The structured caller is the absolute path (unambiguous for machine consumption); the colored handler is the one that shortens it for humans.

The event uses the message strings liteorm.query / liteorm.exec and the attribute keys sql, args, dur, rows, caller, err — all exported as constants (liteorm.MsgQuery, liteorm.AttrSQL, …) so a custom slog handler can match and format them.

Logging is purely level-gated:

  • On — give LiteORM a logger enabled for slog.LevelDebug (devlog.New(...) is at debug by default; for a standard handler set Level: slog.LevelDebug).
  • Off — use any logger above debug (the default slog.Default() is at info), and statements are not logged and not timed.

So a common pattern is to enable debug logging via a flag or env var:

var lg *slog.Logger
if os.Getenv("APP_DEBUG") != "" {
lg = devlog.New(os.Stderr, nil)
} else {
lg = slog.New(slog.NewJSONHandler(os.Stderr, nil)) // info+ only
}
db, _ := sqlite.Open("app.db", liteorm.WithLogger(lg))

By default the bind argument values are logged — that is what makes a statement traceable and reproducible. If some statements carry secrets and you log at debug in a sensitive environment, redact the values (only the count is logged) with liteorm.WithSQLArgs(false):

db, _ := sqlite.Open("app.db", liteorm.WithLogger(devlog.New(os.Stderr, nil)), liteorm.WithSQLArgs(false))

Statements run through a liteorm.Session (a *DB or a transaction) are logged — that covers both the query and orm front-ends and raw ExecContext/QueryContext/query.Raw, including statements inside a transaction. Runnable demonstration: examples/logging.