Observability
Introduction
Once your program talks to a database, you want to see what it sends: to log statements, trace them, count them in tests, catch N+1 queries and slow ones, and watch the connection pool. The ORM gives each of these a hook.
Observing Statements
Every statement passes through observers — the hook for logging, tracing, metrics, a debug toolbar, or counting queries in tests. Add one like this:
stop := orm.Observe(func(ctx context.Context, q orm.QueryInfo) {
slog.DebugContext(ctx, "sql", "sql", q.SQL, "db", q.DB, "dur", q.Duration, "rows", q.Rows, "err", q.Err)
})
defer stop()
ctx = orm.WithObserver(ctx, logQuery) // one context's statements onlyEach observer receives an orm.QueryInfo:
orm.QueryInfo field | |
|---|---|
SQL, Args | the statement and its arguments |
DB, Replica | the database's name, and whether a replica of it served the read |
Start, Duration | when, how long |
Rows | the rows an exec affected; -1 for a query |
Err | the error |
Refused, Model | a query the ORM refused before sending it (an argument it can't write, a join across databases): Err says why; it never reached the database |
Catching N+1 Queries
orm.WatchRepeats(ctx, n, warn) calls warn once a context runs the same statement n times — the N+1 that SelectRelated or PrefetchRelated would read in one query. This fragment warns when a statement repeats five times:
ctx = orm.WatchRepeats(ctx, 5, func(ctx context.Context, sql string, times int) {
slog.WarnContext(ctx, "repeated query", "sql", sql, "times", times)
})
posts, err := orm.Objects[Post]().All(ctx)
if err != nil {
return err
}
for _, p := range posts {
if _, err := p.Author.Get(ctx).Get(); err != nil { // an N+1: SelectRelated("author") reads it in one
return err
}
}NOTE
Put it on each request's context in development, and on tests' contexts with a warn that fails the test.
Logging Slow Queries
orm.SlowQuery(d) on a declared database logs each statement slower than d, through orm.Logger(l) (else slog.Default()):
analytics := orm.Declare("analytics", os.Getenv("ANALYTICS_URL"), orm.SlowQuery(200*time.Millisecond))Slow queries and state changes are all a supervised database logs.
Watching the Pool
orm.ObserveState(fn) is told every state change of a declared database or replica — Connecting, Up, Degraded, Down — for alerts and metrics:
stop := orm.ObserveState(func(e orm.StateEvent) {
slog.Warn("database", "db", e.DB, "replica", e.Replica, "from", e.From, "to", e.To, "err", e.Err)
})Pool Statistics
Read the statistics with:
orm.Stats(name) | one database of orm.Default: an orm.DBStats |
set.StatsOf(name), set.Stats() | one or every database of a set |
orm.DBStats holds database/sql's pool statistics (Pool), the state, the breaker (closed, open, half-open) and its failures, the last error, and each replica's health and pool. It marshals to JSON — it is what ormweb's /healthz answers in development; elsewhere that route answers each database's state alone.