Observability API Reference
The four ways to see what Quark is doing: query observers (one event per operation), middleware (wrap the execution path), the OpenTelemetry integration, and the slow-query log.
For a worked setup, see Caching & Observability.
| Symbol | Signature | Purpose |
|---|---|---|
QueryObserver | interface — ObserveQuery(event QueryEvent) | Receives a QueryEvent per operation. |
WithQueryObserver | WithQueryObserver(o QueryObserver) Option | Registers an observer on a client. |
Middleware | interface — WrapExec / WrapQuery / WrapQueryRow | Wraps execution functions. |
WithMiddleware | WithMiddleware(m Middleware) Option | Registers middleware on a client. |
WithSlowQueryThreshold | WithSlowQueryThreshold(d time.Duration) Option | Logs a WARN line for operations slower than d. |
quarkotel.New | New(opts ...Option) *Middleware | OpenTelemetry spans + metrics middleware. |
quarkotel.WithSpanRedaction | WithSpanRedaction(mode RedactionMode) Option | RedactArgs (default) or IncludeArgs. |
quarkotel.WithDBSystem | WithDBSystem(name string) Option | Sets the db.system attribute. |
Notify | Notify(ctx, provider ClientProvider, channel, payload string) error | Sends a DB notification (PG only). |
NewListenerFactory | NewListenerFactory(client *Client) *ListenerFactory | Builds the inbound LISTEN/NOTIFY factory. |
QueryObserver
type QueryObserver interface {
ObserveQuery(event QueryEvent)
}
Attach observers with WithQueryObserver:
type MetricsObserver struct{}
func (m *MetricsObserver) ObserveQuery(event quark.QueryEvent) {
metrics.Record(event.Table, event.Operation, event.Duration, event.Rows, event.Error)
}
client, err := quark.New("postgres", dsn,
quark.WithQueryObserver(&MetricsObserver{}),
)
QueryEvent
type QueryEvent struct {
SQL string
Args []any
Duration time.Duration
Rows int64
Error error
Table string
Operation string
}
Operations include values such as SELECT, EXEC, QUERY_ROW,
SELECT (stream), SELECT (cursor), PRELOAD, RAW_QUERY, and RAW_EXEC.
PRELOAD covers the batched eager-loading SELECTs that Preload issues
(… WHERE fk IN (…) — join-table lookups included). They flow through the
same observer pipeline as every other operation, so they also feed the
slow-query log below. Rows is 0 for these events: the rows are consumed
by the relation mapper after the event is emitted.
Middleware
type Middleware interface {
WrapExec(next ExecFunc) ExecFunc
WrapQuery(next QueryFunc) QueryFunc
WrapQueryRow(next QueryRowFunc) QueryRowFunc
}
Function types:
type ExecFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) (sql.Result, error)
type QueryFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) (*sql.Rows, error)
type QueryRowFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) *sql.Row
Embed BaseMiddleware to override only the paths you need:
type LoggingMiddleware struct {
quark.BaseMiddleware
}
func (l *LoggingMiddleware) WrapQuery(next quark.QueryFunc) quark.QueryFunc {
return func(ctx context.Context, exec quark.Executor, sqlStr string, args []any) (*sql.Rows, error) {
start := time.Now()
rows, err := next(ctx, exec, sqlStr, args)
log.Printf("query duration=%s err=%v sql=%s", time.Since(start), err, sqlStr)
return rows, err
}
}
Slow query log
WithSlowQueryThreshold(d) enables a structured WARN log line for every
Quark operation that takes longer than d. The line is emitted through
Client.logger (the configured *slog.Logger) before any registered
QueryObserver is notified.
client, _ := quark.New("pgx", dsn,
quark.WithSlowQueryThreshold(100*time.Millisecond),
)
Fields on the log record:
| Attribute | Type | Source |
|---|---|---|
duration_ms | int64 | The observed duration in milliseconds. |
threshold_ms | int64 | The configured threshold in milliseconds. |
operation | string | The QueryEvent.Operation value — SELECT, SELECT (stream), SELECT (cursor), PRELOAD, QUERY_ROW, EXEC, RAW_QUERY, or RAW_EXEC. |
table | string | The Quark-managed table, when known (raw queries leave it empty). |
rows | int64 | Rows affected for EXEC / rows returned for QUERY_ROW; zero otherwise. |
sql | string | The parameterised SQL. Bind arguments are NOT included — the same redaction principle as OTel spans (below). |
Default: 0 (disabled). A negative value is also treated as disabled.
Callers that want richer context (or argument-aware reporting under their
own retention policy) should register a QueryObserver.
The threshold check is a single comparison; a Client with the feature disabled pays nothing on the observer hot path.
OpenTelemetry
import quarkotel "github.com/jcsvwinston/quark/otel"
client, err := quark.New("postgres", dsn,
quark.WithMiddleware(quarkotel.New()),
)
The middleware emits spans and metrics for every database operation.
Both tracer and meter are resolved lazily from the OTel global providers
at query-execution time — install your TracerProvider / MeterProvider
before the first query.
Spans
| Execution | Span name |
|---|---|
ExecContext | quark.exec |
QueryContext | quark.query |
QueryRowContext | quark.query_row |
Attributes: db.statement (parameterised SQL with placeholders), db.operation
(EXEC / SELECT / SELECT_ROW), and — when WithDBSystem is set —
db.system. Bind arguments are not attached to spans by default.
Span redaction (WithSpanRedaction)
By default the middleware operates in RedactArgs mode: bind arguments
never reach a span. Only the parameterised SQL ends up in db.statement.
For local debugging you can opt in to IncludeArgs, which attaches a
db.statement.args attribute (a string slice rendered via fmt.Sprintf("%v", arg)):
quarkotel.New(quarkotel.WithSpanRedaction(quarkotel.IncludeArgs))
Use IncludeArgs only in development. A tracing backend MUST NOT see user
values it has no authority to retain.
Metrics
The middleware emits three instruments on the github.com/jcsvwinston/quark
meter:
| Instrument | Kind | Unit | Description |
|---|---|---|---|
quark.queries.total | counter (Int64) | — | Every Quark operation increments this. |
quark.queries.duration | histogram (Float64) | ms | Wall-clock time of the operation, including middleware overhead. |
quark.queries.rows | histogram (Int64) | — | Rows affected, Exec only — emitted when sql.Result.RowsAffected() succeeds. SELECT / SELECT_ROW do not emit (counting them would require wrapping *sql.Rows, a future enhancement). |
Every data point carries db.operation and — when WithDBSystem is set —
db.system.
WithDBSystem
The middleware sits below the query builder and does not introspect the
Quark Client. If you want the db.system attribute populated, pass the
dialect name explicitly:
quarkotel.New(quarkotel.WithDBSystem("postgres"))
When unset, the attribute is omitted from spans and metrics.
Hooks
Entity-level hooks:
type BeforeCreateHook interface {
BeforeCreate(ctx context.Context) error
}
Available hook interfaces:
| Interface | Method |
|---|---|
BeforeCreateHook | BeforeCreate(context.Context) error |
AfterCreateHook | AfterCreate(context.Context) error |
BeforeUpdateHook | BeforeUpdate(context.Context) error |
AfterUpdateHook | AfterUpdate(context.Context) error |
BeforeDeleteHook | BeforeDelete(context.Context) error |
AfterDeleteHook | AfterDelete(context.Context) error |
Notifications
Notify(ctx context.Context, provider ClientProvider, channel, payload string) error
Sends a database notification where supported.
err := quark.Notify(ctx, client, "user_events", `{"type":"signup","id":123}`)
Current support:
| Dialect | Behavior |
|---|---|
| PostgreSQL | Uses pg_notify($1, $2). |
| MySQL | Returns not supported. |
| SQLite | Returns not supported. |
| Other dialects | Returns not supported. |
NewListenerFactory(client *Client) *ListenerFactory
Creates the inbound LISTEN/NOTIFY listener factory. On PostgreSQL,
CreateListener returns a working inbound LISTEN/NOTIFY listener — a
dedicated connection, fire-and-forget. On the other five engines it returns
ErrDialectNotSupported. See inbound events for
usage. This is unrelated to the outbound CRUD-event
EventBus interface.