Skip to main content
Version: 1.10.0

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.

SymbolSignaturePurpose
QueryObserverinterface — ObserveQuery(event QueryEvent)Receives a QueryEvent per operation.
WithQueryObserverWithQueryObserver(o QueryObserver) OptionRegisters an observer on a client.
Middlewareinterface — WrapExec / WrapQuery / WrapQueryRowWraps execution functions.
WithMiddlewareWithMiddleware(m Middleware) OptionRegisters middleware on a client.
WithSlowQueryThresholdWithSlowQueryThreshold(d time.Duration) OptionLogs a WARN line for operations slower than d.
quarkotel.NewNew(opts ...Option) *MiddlewareOpenTelemetry spans + metrics middleware.
quarkotel.WithSpanRedactionWithSpanRedaction(mode RedactionMode) OptionRedactArgs (default) or IncludeArgs.
quarkotel.WithDBSystemWithDBSystem(name string) OptionSets the db.system attribute.
NotifyNotify(ctx, provider ClientProvider, channel, payload string) errorSends a DB notification (PG only).
NewListenerFactoryNewListenerFactory(client *Client) *ListenerFactoryBuilds 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:

AttributeTypeSource
duration_msint64The observed duration in milliseconds.
threshold_msint64The configured threshold in milliseconds.
operationstringThe QueryEvent.Operation value — SELECT, SELECT (stream), SELECT (cursor), PRELOAD, QUERY_ROW, EXEC, RAW_QUERY, or RAW_EXEC.
tablestringThe Quark-managed table, when known (raw queries leave it empty).
rowsint64Rows affected for EXEC / rows returned for QUERY_ROW; zero otherwise.
sqlstringThe 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

ExecutionSpan name
ExecContextquark.exec
QueryContextquark.query
QueryRowContextquark.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:

InstrumentKindUnitDescription
quark.queries.totalcounter (Int64)Every Quark operation increments this.
quark.queries.durationhistogram (Float64)msWall-clock time of the operation, including middleware overhead.
quark.queries.rowshistogram (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:

InterfaceMethod
BeforeCreateHookBeforeCreate(context.Context) error
AfterCreateHookAfterCreate(context.Context) error
BeforeUpdateHookBeforeUpdate(context.Context) error
AfterUpdateHookAfterUpdate(context.Context) error
BeforeDeleteHookBeforeDelete(context.Context) error
AfterDeleteHookAfterDelete(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:

DialectBehavior
PostgreSQLUses pg_notify($1, $2).
MySQLReturns not supported.
SQLiteReturns not supported.
Other dialectsReturns 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.