Skip to content

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:

go
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 only

Each observer receives an orm.QueryInfo:

orm.QueryInfo field
SQL, Argsthe statement and its arguments
DB, Replicathe database's name, and whether a replica of it served the read
Start, Durationwhen, how long
Rowsthe rows an exec affected; -1 for a query
Errthe error
Refused, Modela 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:

go
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()):

go
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:

go
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.

Released under the MIT License. Vuka builds on Go and its tools, which are made by the Go Authors. Vuka is not affiliated with Google or the Go project. Go is a trademark of Google LLC.