Documentation
¶
Overview ¶
Package querytrace is an experimental, application-level SQL tracing middleware for database/sql. It records the queries a unit of work issues together with their application context — the operation, the calling code, the transaction, and how the result rows were consumed — and surfaces problems that database-side statistics cannot see: N+1 patterns, queries in a loop, query-count blow-ups, writes where only reads were expected, and rows left unclosed.
It is not an execution-plan analyzer or an APM agent. Plan/index/lock analysis belongs to the database and to OpenTelemetry/APM; querytrace answers a different question — "what did this request/usecase ask the database to do, and from where?"
The center of the design is the Trace, a per-operation recorder placed in a context. A wrapped database/sql driver (subpackage driver) records an Event per statement against the Trace it finds in the context, and a Trace turns its Events into a Report and a set of Warnings. SQL is analyzed best-effort through an injected sqlparse.Parser; analysis never aborts execution.
Index ¶
- func ForbidQueries(ctx context.Context) context.Context
- func ForbidWrites(ctx context.Context) context.Context
- func Mark(ctx context.Context, name string, attrs ...Attr)
- func Named(ctx context.Context, name string, fn func(ctx context.Context) error) error
- func ReadOnly(ctx context.Context) context.Context
- func TextReport(t *Trace) string
- func WantCaller(mode CallerMode, err error, dur, slow time.Duration) bool
- func WithLabel(ctx context.Context, key, value string) context.Context
- func WithOperation(ctx context.Context, name string) context.Context
- func WithQueryName(ctx context.Context, name string) context.Context
- func WithTrace(ctx context.Context, trace *Trace) context.Context
- type ArgCaptureMode
- type Attr
- type Caller
- type CallerMode
- type Config
- type Event
- type EventKind
- type MarkInfo
- type NPlusOneConfig
- type Option
- type QueryInfo
- type Report
- type RowsInfo
- type Severity
- type Shape
- type StmtKind
- type Trace
- func (t *Trace) EnsureConfig(cfg Config)
- func (t *Trace) Events() []Event
- func (t *Trace) ID() string
- func (t *Trace) Marks() []MarkInfo
- func (t *Trace) Operation() string
- func (t *Trace) RecordStatement(ctx context.Context, ev Event) string
- func (t *Trace) RecordTxBegin() string
- func (t *Trace) RecordTxEnd(action TxAction, txID string)
- func (t *Trace) Report() Report
- func (t *Trace) StartedAt() time.Time
- func (t *Trace) Warnings() []Warning
- type TxAction
- type TxInfo
- type Warning
- type WarningCode
- type WarningConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ForbidQueries ¶
ForbidQueries marks ctx as forbidding all database access: any statement run under it raises query_forbidden. It asserts a path is served without touching the database, such as a cache hit.
func ForbidWrites ¶
ForbidWrites marks ctx as forbidding writes: a write run under it raises write_forbidden. Reads are allowed.
func Mark ¶
Mark places a named marker on the context's Trace timeline, interleaving an application step (a cache miss, an external call) with the queries around it. It is a no-op when ctx carries no Trace.
func Named ¶
Named runs fn with name attached to the statements it issues, scoping the query name to exactly one call site. It is the function-shaped form of WithQueryName:
err := querytrace.Named(ctx, "UserRepository.FindByID", func(ctx context.Context) error {
return repo.FindByID(ctx, id)
})
func ReadOnly ¶
ReadOnly marks ctx as read-only: a write run under it raises write_in_read_only. Reads are allowed. It is convenient in tests that assert a code path does not mutate the database.
func TextReport ¶
TextReport renders a human-readable report for the Trace, the form most useful in development logs and test failures.
func WantCaller ¶
func WantCaller(mode CallerMode, err error, dur, slow time.Duration) bool
WantCaller reports whether a statement with the given outcome should have its caller captured under mode. It is part of the instrumentation contract used by wrapped drivers.
func WithLabel ¶
WithLabel attaches a key/value label to ctx, copied onto every Event recorded under it. Labels accumulate; later keys override earlier ones.
func WithOperation ¶
WithOperation sets the operation name on the context's Trace, naming the unit of work after it has been created (for example once an HTTP route is known). It is a no-op when ctx carries no Trace.
func WithQueryName ¶
WithQueryName attaches a query name to ctx, applied to the statements run under it (for example "UserRepository.FindByID"). The name is sticky: it stays until overridden. Prefer Named when the name should apply to a single call.
Types ¶
type ArgCaptureMode ¶
type ArgCaptureMode int
ArgCaptureMode controls how much of a statement's bind arguments are kept. Values are sensitive, so the default keeps none.
const ( // ArgsOff keeps no argument information beyond the count. ArgsOff ArgCaptureMode = iota // ArgsTypesOnly keeps the Go type name of each argument, not its value. ArgsTypesOnly )
type Caller ¶
type Caller struct {
// Function is the fully qualified function name.
Function string
// File is the source file path.
File string
// Line is the line number.
Line int
}
Caller is the application code that issued a statement.
func CaptureCaller ¶
func CaptureCaller() *Caller
CaptureCaller walks the stack and returns the first frame outside querytrace and the database/sql machinery — the application code that issued the statement. It returns nil when no such frame is found. It is part of the instrumentation contract used by wrapped drivers; call it from the driver method that runs the statement so the application frame is reached.
type CallerMode ¶
type CallerMode int
CallerMode controls when the wrapped driver captures the calling code for an Event. Capturing a caller walks the stack, so the cost is paid only when the mode says it is worth it.
const ( // CallerOff never captures a caller. CallerOff CallerMode = iota // CallerOnError captures a caller only for statements that errored. CallerOnError // CallerOnSlowQuery captures a caller for errors and for statements slower // than Config.SlowQueryThreshold. CallerOnSlowQuery // CallerAlways captures a caller for every statement. Best for development // and tests, where caller attribution drives N+1 and loop detection. CallerAlways )
type Config ¶
type Config struct {
// Enabled turns tracing on. A disabled Config makes the wrapped driver a
// thin pass-through.
Enabled bool
// Dialect is the SQL dialect used to recompile a parsed query into its
// fingerprint. It defaults to PostgreSQL when nil.
Dialect sql.Dialect
// Parser analyzes SQL text into a sql.Query for structural analysis and
// fingerprinting. It is best-effort: a parse failure or a nil Parser falls
// back to lightweight string analysis and never aborts execution.
//
// DebugConfig wires this automatically from its dialect argument, provided a
// frontend such as github.com/aita/sqlkit/x/sqlparse/postgres is
// blank-imported. Set it explicitly to override that, for example to use a
// custom parser.
Parser sqlparse.Parser
// CaptureRawSQL keeps the executed SQL text on each QueryInfo. Leave it off
// for production exporters; raw SQL can carry sensitive shapes.
CaptureRawSQL bool
// CaptureArgs controls how much of the bind arguments are kept.
CaptureArgs ArgCaptureMode
// CaptureCaller controls when the calling code is captured.
CaptureCaller CallerMode
// Fingerprint enables SQL fingerprinting, the basis of repeated-query and
// N+1 detection.
Fingerprint bool
// AnalyzeSQL enables structural analysis (kind, tables, WHERE/JOIN/...),
// the basis of write/where warnings.
AnalyzeSQL bool
// SlowQueryThreshold is the duration above which a query counts as slow,
// for CallerOnSlowQuery.
SlowQueryThreshold time.Duration
// NPlusOne configures repeated-query and loop detection.
NPlusOne NPlusOneConfig
// Warnings configures the threshold-based warnings.
Warnings WarningConfig
}
Config configures a wrapped driver and the Traces it records. The zero value is usable but minimal; DebugConfig is the recommended starting point for development and tests.
func DebugConfig ¶
DebugConfig returns a Config tuned for development and tests for the named dialect ("postgres", "mysql", or "sqlite"; "" and unknown names default to postgres): raw SQL and argument types captured, callers always captured, fingerprinting and analysis on, and N+1 detection enabled. It captures more than is appropriate for production.
The matching sql.Dialect is stored in Dialect, and a parser registered for that dialect — via a blank-imported frontend such as github.com/aita/sqlkit/x/sqlparse/postgres — is wired into Parser automatically. Without such an import Parser stays nil and analysis falls back to lightweight string analysis.
func (Config) Analyze ¶
Analyze produces the structural QueryInfo for a statement from its SQL text and bind arguments. It is best-effort and never returns an error: a parse failure is recorded in AnalyzeErr and the analysis falls back to a keyword guess, so observability never interferes with execution. It is part of the instrumentation contract used by wrapped drivers.
type Event ¶
type Event struct {
// ID identifies the event within its Trace.
ID string
// Kind is the kind of event.
Kind EventKind
// Name is an optional application label for the event, such as a query
// name ("UserRepository.FindByID") set with WithQueryName or Named.
Name string
// StartedAt is when the operation began.
StartedAt time.Time
// EndedAt is when it finished; zero until then.
EndedAt time.Time
// Duration is EndedAt-StartedAt, the time the statement took to return
// (for a query, the time until the rows cursor is returned — not the time
// to iterate it; see Rows).
Duration time.Duration
// Query carries SQL details for EventQuery and EventExec.
Query *QueryInfo
// Tx carries transaction details for EventTx.
Tx *TxInfo
// Rows carries row-cursor lifecycle details for a read whose cursor was
// wrapped; nil for execs and for reads that returned no wrapped cursor.
Rows *RowsInfo
// Caller is the application code that issued the statement, captured
// subject to Config.CaptureCaller.
Caller *Caller
// Labels are context labels attached with WithLabel.
Labels map[string]string
// Err is the error the statement returned, if any.
Err error
}
Event is one recorded point on a Trace's timeline: a query, an exec, a transaction control point, or a mark. Events are ordered by StartedAt.
type EventKind ¶
type EventKind string
EventKind is the kind of thing an Event records. SQL reads and writes are the common cases; the kind is an enum so transaction and mark events can share the timeline.
const ( // EventQuery is a row-returning read (SELECT and other reads). EventQuery EventKind = "query" // EventExec is a write returning a result (INSERT/UPDATE/DELETE/DDL). EventExec EventKind = "exec" // EventTx is a transaction control point (begin/commit/rollback). EventTx EventKind = "tx" // EventMark is an application-supplied marker on the timeline. EventMark EventKind = "mark" )
type MarkInfo ¶
type MarkInfo struct {
// Name is the marker text.
Name string
// At is when the mark was placed.
At time.Time
// Attrs are optional key/value details.
Attrs map[string]string
}
MarkInfo is an application event placed on the timeline with Mark, such as "calling payment API" or "cache miss". Marks make a transaction's timeline readable by interleaving non-SQL steps with the queries around them.
type NPlusOneConfig ¶
type NPlusOneConfig struct {
// Enabled turns repeated-query and loop detection on.
Enabled bool
// MinCount is how many times one fingerprint must repeat before it is
// reported. It defaults to 10 when zero.
MinCount int
// IgnoreOperations skips fingerprints of these kinds; repeated INSERTs into
// a log table, for example, are usually expected.
IgnoreOperations []StmtKind
}
NPlusOneConfig configures detection of repeated query shapes within one Trace.
type Option ¶
type Option func(*Trace)
Option configures a Trace at creation.
func WithConfig ¶
WithConfig sets the Trace's warning configuration up front. Normally a wrapped driver stamps its own Config onto the Trace on first use, so this is mainly for tests that build a Trace directly.
type QueryInfo ¶
type QueryInfo struct {
// RawSQL is the executed SQL text, kept only when Config.CaptureRawSQL is
// set; otherwise empty.
RawSQL string
// Fingerprint is the normalized SQL with literals and placeholders
// collapsed, used to group repeated queries. It is empty when
// fingerprinting is disabled.
Fingerprint string
// Operation is the statement kind, from analysis or a best-effort guess.
Operation StmtKind
// Tables are the tables the statement references, when analysis succeeded.
Tables []string
// HasWhere, HasJoin, HasLimit, HasReturning mirror Shape; they are
// false when analysis could not determine them.
HasWhere bool
HasJoin bool
HasLimit bool
HasReturning bool
// ParamCount is the number of bind arguments.
ParamCount int
// ParamTypes are the Go type names of the bind arguments, kept when
// Config.CaptureArgs is ArgsTypesOnly or higher.
ParamTypes []string
// TxID links the statement to its transaction, empty outside one.
TxID string
// AnalyzeErr records why structural analysis fell back to a guess, for
// diagnostics. It is never an execution error.
AnalyzeErr error
// contains filtered or unexported fields
}
QueryInfo holds the SQL details of an EventQuery or EventExec. Raw SQL and argument values are captured only when configuration allows; the structural fields (kind, tables, shape, parameter count) are always safe to keep.
type Report ¶
type Report struct {
// TraceID is the trace's identifier.
TraceID string
// Operation is the unit of work the trace covers.
Operation string
// StartedAt is when the Trace was created, the start of the unit of work.
StartedAt time.Time
// QueryCount is the number of read and write statements.
QueryCount int
// WriteCount is the number of write statements.
WriteCount int
// UniqueFingerprintCount is the number of distinct query shapes.
UniqueFingerprintCount int
// TotalQueryDuration is the summed time statements took to return.
TotalQueryDuration time.Duration
// TotalRowsIterationDuration is the summed time spent iterating row
// cursors.
TotalRowsIterationDuration time.Duration
// Events is the recorded timeline, in record order.
Events []Event
// Marks is the recorded application marks.
Marks []MarkInfo
// Warnings is every warning for the trace.
Warnings []Warning
}
Report is the summary of a Trace: the counts and durations of its statements, the timeline of events and marks, and the warnings. It is a plain value, safe to marshal to JSON or render with TextReport.
type RowsInfo ¶
type RowsInfo struct {
// QueryReturnedAt is when QueryContext handed back the cursor.
QueryReturnedAt time.Time
// FirstNextAt is when the first row was read; zero if none was.
FirstNextAt time.Time
// LastNextAt is when the last row was read.
LastNextAt time.Time
// ClosedAt is when the cursor was closed; zero if it never was.
ClosedAt time.Time
// NextCount is the number of rows read from the cursor.
NextCount int
// Closed reports whether the cursor was closed.
Closed bool
// IterErr is the error that ended row iteration, if any: the first non-EOF
// error returned by Next, or — when database/sql closed the cursor in
// response to a context cancellation rather than surfacing an error through
// Next — the context's error captured at Close. It is nil when the cursor was
// drained to completion. A context.Canceled or context.DeadlineExceeded here
// means the request was abandoned mid-stream, after the database had begun
// returning rows.
IterErr error
// IterationDuration is LastNextAt-QueryReturnedAt, the time spent reading
// rows.
IterationDuration time.Duration
// OpenDuration is ClosedAt-QueryReturnedAt, how long the cursor stayed
// open.
OpenDuration time.Duration
}
RowsInfo records the lifecycle of a query's row cursor — the cost that QueryContext alone hides. The difference between the query returning, the rows being iterated, and the cursor being closed is what distinguishes a fast query with a slow scan from a genuinely slow query.
type Shape ¶
type Shape struct {
// Kind is the statement kind.
Kind StmtKind
// Tables are the base tables the statement references anywhere in its tree,
// in source order with duplicates removed. A reference to a CTE is not a base
// table, so CTE names are excluded.
Tables []string
// HasWhere reports whether the top-level SELECT/UPDATE/DELETE carries a WHERE
// clause. It backs the update_without_where / delete_without_where checks. It
// is always false for a set operation, which has no top-level WHERE.
HasWhere bool
// HasJoin reports whether the top-level statement carries a JOIN clause.
HasJoin bool
// HasLimit reports whether the top-level SELECT or set operation carries a
// LIMIT. A LIMIT on a UNION arm belongs to that arm and is not reflected here.
HasLimit bool
// HasReturning reports whether a write carries a RETURNING clause.
HasReturning bool
}
Shape is a read-only description of a parsed query's structure, used to drive tracing and analysis without recompiling the query or re-parsing its SQL.
Two scopes are at play, by design:
- Tables is aggregated across the whole tree: it follows FROM/JOIN subqueries, set-operation arms, CTE bodies, and the subqueries embedded in expressions, because "what does this statement touch?" wants every base table.
- The boolean flags describe the top-level statement node only. A WHERE, JOIN, or LIMIT inside a subquery, a CTE body, or one arm of a UNION is that nested query's property, not the outer statement's.
So a UNION reports HasWhere/HasJoin false and HasLimit only for a LIMIT on the UNION itself, even when its arms filter, join, or limit.
func Describe ¶
Describe returns the Shape of a parsed query. ok is false when q is nil or of an unrecognized type. It reads the query's structure only, through the sql package's read-only introspection seam (Query.Parts, sql.WalkSubqueries), never compiling or mutating q.
Tables follows FROM/JOIN subqueries, set-operation arms, CTE bodies, and the subqueries embedded in WHERE/SELECT/HAVING expressions (correlated ones included). A reference to a CTE is not a base table, so CTE names are excluded.
type StmtKind ¶
type StmtKind int
StmtKind classifies a SQL statement by the kind of statement it represents. It is a read-only label for tracing and analysis (query budgets, N+1 detection, read-only enforcement) derived from a parsed query's shape.
const ( // StmtUnknown is a statement of an unrecognized kind, or one that could not // be analyzed. StmtUnknown StmtKind = iota // StmtSelect is a row-returning read: SELECT, a set operation (UNION etc.), // or a standalone VALUES. StmtSelect // StmtInsert is an INSERT. StmtInsert // StmtUpdate is an UPDATE. StmtUpdate // StmtDelete is a DELETE. StmtDelete // StmtDDL is a schema statement (CREATE/ALTER/DROP/TRUNCATE/COMMENT). StmtDDL )
type Trace ¶
type Trace struct {
// contains filtered or unexported fields
}
Trace records the statements of one unit of work — a request, a job, a usecase — and the warnings querytrace derives from them. Place a Trace in a context with WithTrace; a wrapped driver then records an Event per statement against it. A Trace is safe for concurrent use, so parallel queries within one operation record correctly.
func FromContext ¶
FromContext returns the Trace carried by ctx, if any.
func (*Trace) EnsureConfig ¶
EnsureConfig stamps cfg onto the Trace the first time it is recorded against, making the wrapping driver's Config the source of truth for warning thresholds. It is part of the instrumentation contract used by wrapped drivers; applications do not call it.
func (*Trace) Events ¶
Events returns a copy of the recorded statement and transaction events, in record order.
func (*Trace) RecordStatement ¶
RecordStatement records a completed query or exec event, filling its name and labels from ctx and running the immediate per-statement checks against the context's expectations. It returns the assigned event ID. It is part of the instrumentation contract used by wrapped drivers.
func (*Trace) RecordTxBegin ¶
RecordTxBegin allocates a transaction identifier, records a begin event, and returns the identifier for the wrapped transaction to carry. It is part of the instrumentation contract used by wrapped drivers.
func (*Trace) RecordTxEnd ¶
RecordTxEnd records a commit or rollback event for txID, carrying the transaction's start time so the timeline can show its span. It is part of the instrumentation contract used by wrapped drivers.
func (*Trace) StartedAt ¶
StartedAt returns when the Trace was created, the start of the unit of work it records. Exporters use it as the start of the operation-level span.
func (*Trace) Warnings ¶
Warnings returns every warning for the Trace: the per-statement warnings raised as it ran, plus the whole-trace warnings (N+1, query-in-loop, too-many-queries, rows-not-closed) derived from its events. It is safe to call repeatedly; derived warnings are recomputed each time, not duplicated.
type TxInfo ¶
type TxInfo struct {
// TxID identifies the transaction within the Trace.
TxID string
// Action is the control point this event records.
Action TxAction
// StartedAt is when the transaction began; set on commit/rollback events
// so the timeline can show the transaction's duration.
StartedAt time.Time
}
TxInfo holds the details of an EventTx.
type Warning ¶
type Warning struct {
// Code identifies the kind of warning.
Code WarningCode
// Message is a human-readable description.
Message string
// Severity ranks the warning.
Severity Severity
// EventID links the warning to the Event that triggered it, when one did.
EventID string
// Caller is the application code involved, when known.
Caller *Caller
// Details carries structured specifics (fingerprint, count, ...).
Details map[string]any
}
Warning is one problem querytrace found in a Trace. Warnings are advisory: a Trace records them, exporters surface them, and querytest asserts on them, but they never interrupt execution.
type WarningCode ¶
type WarningCode string
WarningCode identifies a kind of warning. Codes are stable strings so they can be matched in tests and exporters.
const ( // WarnPossibleNPlusOne marks a fingerprint repeated above the N+1 // threshold within a Trace. WarnPossibleNPlusOne WarningCode = "possible_n_plus_one" // WarnQueryInLoop marks the same fingerprint repeated from the same caller, // the signature of a query inside a loop. WarnQueryInLoop WarningCode = "query_in_loop" // WarnTooManyQueries marks a Trace exceeding its query budget. WarnTooManyQueries WarningCode = "too_many_queries" // WarnWriteInReadOnly marks a write under a ReadOnly context. WarnWriteInReadOnly WarningCode = "write_in_read_only" // WarnQueryForbidden marks any statement under a ForbidQueries context. WarnQueryForbidden WarningCode = "query_forbidden" // WarnWriteForbidden marks a write under a ForbidWrites context. WarnWriteForbidden WarningCode = "write_forbidden" // WarnUpdateWithoutWhere marks an UPDATE with no WHERE clause. WarnUpdateWithoutWhere WarningCode = "update_without_where" // WarnDeleteWithoutWhere marks a DELETE with no WHERE clause. WarnDeleteWithoutWhere WarningCode = "delete_without_where" // WarnRowsNotClosed marks a row cursor that was never closed. WarnRowsNotClosed WarningCode = "rows_not_closed" // WarnQueryCanceled marks a row cursor abandoned through context // cancellation while it was still being iterated. Execution-level // cancellation, slow queries, and plain errors are left to OpenTelemetry // tracing, which records them as span duration and status; the mid-iteration // case is invisible to a query span that has already ended. WarnQueryCanceled WarningCode = "query_canceled" // WarnQueryDeadlineExceeded marks a row cursor that hit its context deadline // while it was still being iterated. See WarnQueryCanceled on scope. WarnQueryDeadlineExceeded WarningCode = "query_deadline_exceeded" // WarnTooManyParameters marks a statement carrying at or above the bind // parameter budget, the signature of a large IN list or oversized batch. WarnTooManyParameters WarningCode = "too_many_parameters" )
type WarningConfig ¶
type WarningConfig struct {
// MaxQueries is the per-Trace query count above which too_many_queries
// fires. Zero disables the check.
MaxQueries int
// MaxParams is the bind-parameter count at or above which a statement raises
// too_many_parameters. Zero disables the check.
MaxParams int
}
WarningConfig configures the threshold-based warnings.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package driver wraps a database/sql/driver so that statements run through it are recorded on the querytrace.Trace carried by their context.
|
Package driver wraps a database/sql/driver so that statements run through it are recorded on the querytrace.Trace carried by their context. |
|
Package querytest turns a querytrace.Trace into a test spy: it records the SQL one operation actually issued, and this package asserts on it after the fact.
|
Package querytest turns a querytrace.Trace into a test spy: it records the SQL one operation actually issued, and this package asserts on it after the fact. |
|
Package slogtrace exports a querytrace.Trace to a log/slog logger.
|
Package slogtrace exports a querytrace.Trace to a log/slog logger. |