Documentation
¶
Overview ¶
Package querytrace is an 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 ¶
- Constants
- 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 SetOperation(ctx context.Context, name string)
- 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 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 ConfigOption
- func WithArgCapture(mode ArgCaptureMode) ConfigOption
- func WithCaller(mode CallerMode) ConfigOption
- func WithDialect(d sql.Dialect) ConfigOption
- func WithMaxInList(n int) ConfigOption
- func WithMaxParams(n int) ConfigOption
- func WithMaxQueries(n int) ConfigOption
- func WithNPlusOneIgnore(ops ...StmtKind) ConfigOption
- func WithNPlusOneThreshold(minCount int) ConfigOption
- func WithParser(p sqlparse.Parser) ConfigOption
- func WithRawSQL() ConfigOption
- func WithSelectStarWarning() ConfigOption
- func WithSlowQueryThreshold(d time.Duration) ConfigOption
- func WithSoftDeleteCheck(opts ...PredicateOption) ConfigOption
- func WithTenantCheck(opts ...PredicateOption) ConfigOption
- func WithVerboseCapture() ConfigOption
- func WithoutAnalyze() ConfigOption
- func WithoutFingerprint() ConfigOption
- func WithoutNPlusOne() ConfigOption
- type Event
- type EventKind
- type ExpvarRecorder
- type Label
- type LogExporter
- type LogMode
- type LogOption
- type MarkInfo
- type Metrics
- type MetricsRecorder
- type Option
- type PredicateOption
- type QueryInfo
- type Report
- type RowsInfo
- type Sampler
- type SamplerFunc
- type Severity
- 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) Metrics() Metrics
- func (t *Trace) Operation() string
- func (t *Trace) RecordMetrics(rec MetricsRecorder)
- 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) Sampled() bool
- func (t *Trace) StartedAt() time.Time
- func (t *Trace) Warnings() []Warning
- type TxAction
- type TxInfo
- type Warning
- type WarningCode
Constants ¶
const ( StmtUnknown = analysis.StmtUnknown StmtSelect = analysis.StmtSelect StmtInsert = analysis.StmtInsert StmtUpdate = analysis.StmtUpdate StmtDelete = analysis.StmtDelete StmtDDL = analysis.StmtDDL )
The statement kinds, re-exported from analyze so querytrace callers (and querytest matchers) refer to them without importing analyze directly.
const ( MetricStatements = "querytrace.statements" // counter, label "kind" MetricUniqueFingerprint = "querytrace.unique_fingerprints" // counter MetricTransactions = "querytrace.transactions" // counter MetricWarnings = "querytrace.warnings" // counter, label "code" MetricQueryDuration = "querytrace.query_duration" // duration MetricRowsIteration = "querytrace.rows_iteration_duration" // duration )
Metric names emitted by Metrics.Emit. They are stable strings so dashboards and alerts can rely on them.
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 SetOperation ¶
SetOperation 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). Like Mark, it acts on the Trace already carried by ctx rather than returning a new context, and is a no-op when ctx carries no Trace.
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 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 the slow-query threshold (WithSlowQueryThreshold). 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 {
// contains filtered or unexported fields
}
Config configures a wrapped driver and the Traces it records. It is an opaque value built by NewConfig and the With* options; the zero value is a disabled, pass-through Config.
func NewConfig ¶
func NewConfig(opts ...ConfigOption) Config
NewConfig returns a Config built from opts. With no options it is enabled with production-minded defaults: no raw SQL, argument *types* only (never values), callers captured on error, fingerprinting, structural analysis, and N+1 detection on, and the PostgreSQL dialect. Layer options to capture more for development (WithVerboseCapture) or to adjust any single setting.
// Development: capture everything, wire a parser for structural analysis. cfg := querytrace.NewConfig( querytrace.WithParser(postgres.New()), querytrace.WithVerboseCapture(), ) // Production: defaults are already safe; just wire the parser. cfg := querytrace.NewConfig(querytrace.WithParser(postgres.New()))
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.
func (Config) Dialect ¶
Dialect returns the configured SQL dialect, defaulting to PostgreSQL when none was set. It never returns nil.
func (Config) Enabled ¶
Enabled reports whether the Config records traces. A disabled Config (the zero value) makes the wrapped driver a thin pass-through.
func (Config) WantCaller ¶
WantCaller reports whether a statement that finished with err after dur should have its calling code captured, under the configured CallerMode and slow-query threshold. A wrapped driver consults it before walking the stack.
type ConfigOption ¶
type ConfigOption func(*Config)
ConfigOption configures a Config built by NewConfig. Options compose, so a caller layers only the choices that differ from the defaults.
func WithArgCapture ¶
func WithArgCapture(mode ArgCaptureMode) ConfigOption
WithArgCapture controls how much of the bind arguments are kept.
func WithCaller ¶
func WithCaller(mode CallerMode) ConfigOption
WithCaller controls when the calling code is captured.
func WithDialect ¶
func WithDialect(d sql.Dialect) ConfigOption
WithDialect sets the SQL dialect used to recompile parsed queries into fingerprints. A nil dialect is ignored, leaving the PostgreSQL default.
func WithMaxInList ¶
func WithMaxInList(n int) ConfigOption
WithMaxInList sets the IN (...) value-list budget: a statement with an IN list of n or more items raises large_in_list. It reads the parsed AST, so it stays silent on the keyword-fallback path. Zero (the default) disables the check.
func WithMaxParams ¶
func WithMaxParams(n int) ConfigOption
WithMaxParams sets the bind-parameter budget: a statement carrying n or more bind parameters raises too_many_parameters. Zero (the default) disables the check.
func WithMaxQueries ¶
func WithMaxQueries(n int) ConfigOption
WithMaxQueries sets the per-Trace query-count budget: a Trace with more read and write statements than n raises too_many_queries. Zero (the default) disables the check.
func WithNPlusOneIgnore ¶
func WithNPlusOneIgnore(ops ...StmtKind) ConfigOption
WithNPlusOneIgnore excludes the given statement kinds from repeated-query detection; repeated INSERTs into a log table, for example, are usually expected.
func WithNPlusOneThreshold ¶
func WithNPlusOneThreshold(minCount int) ConfigOption
WithNPlusOneThreshold sets how many times one query shape must repeat within a Trace before it is reported as a query-in-loop or possible N+1. Detection is on by default at a threshold of 10; a value <= 0 restores that default.
func WithParser ¶
func WithParser(p sqlparse.Parser) ConfigOption
WithParser sets the parser that drives structural analysis and fingerprinting. Pass a frontend's parser, for example postgres.New() from github.com/aita/sqlkit/sqlparse/postgres. Without a parser, analysis falls back to lightweight string heuristics.
func WithRawSQL ¶
func WithRawSQL() ConfigOption
WithRawSQL keeps the executed SQL text on each statement. Leave it off for production exporters; raw SQL can carry sensitive shapes.
func WithSelectStarWarning ¶
func WithSelectStarWarning() ConfigOption
WithSelectStarWarning enables select_star, which fires on a SELECT * query. It reads the parsed AST, so it stays silent on the keyword-fallback path.
func WithSlowQueryThreshold ¶
func WithSlowQueryThreshold(d time.Duration) ConfigOption
WithSlowQueryThreshold sets the duration above which a query counts as slow.
func WithSoftDeleteCheck ¶
func WithSoftDeleteCheck(opts ...PredicateOption) ConfigOption
WithSoftDeleteCheck enables missing_soft_delete_predicate: a SELECT touching a base table whose top-level WHERE references none of the configured marker columns. Configure it with Columns (required) and, optionally, ScopedToSchema. The check reads the parsed AST, so it stays silent on the keyword-fallback path.
querytrace.WithSoftDeleteCheck(querytrace.Columns("deleted_at"), querytrace.ScopedToSchema(md))
func WithTenantCheck ¶
func WithTenantCheck(opts ...PredicateOption) ConfigOption
WithTenantCheck enables missing_tenant_predicate: a read or write touching a base table whose top-level WHERE references none of the configured marker columns. Configure it with Columns (required) and, optionally, ScopedToSchema. The check reads the parsed AST, so it stays silent on the keyword-fallback path.
querytrace.WithTenantCheck(querytrace.Columns("tenant_id"), querytrace.ScopedToSchema(md))
func WithVerboseCapture ¶
func WithVerboseCapture() ConfigOption
WithVerboseCapture turns on the development-grade capture the safe defaults leave off: raw SQL and a caller for every statement. It captures more than is appropriate for production.
func WithoutAnalyze ¶
func WithoutAnalyze() ConfigOption
WithoutAnalyze turns off structural analysis. Analysis is on by default and backs the predicate and shape warnings (large_in_list, select_star, the tenant/soft-delete predicates), so disabling it also disables those.
func WithoutFingerprint ¶
func WithoutFingerprint() ConfigOption
WithoutFingerprint turns off SQL fingerprinting. Fingerprinting is on by default and underpins repeated-query and N+1 detection, so disabling it also disables those.
func WithoutNPlusOne ¶
func WithoutNPlusOne() ConfigOption
WithoutNPlusOne turns off repeated-query and loop detection, which is on by default.
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 the configured CallerMode (WithCaller).
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 ExpvarRecorder ¶
type ExpvarRecorder struct {
// contains filtered or unexported fields
}
ExpvarRecorder is a MetricsRecorder that publishes a trace's metrics to an expvar.Map, exposing them on the standard /debug/vars endpoint with nothing but the standard library. Counters accumulate as expvar integers and durations as a running total in seconds (expvar has no histogram, so the distribution is not retained — only the sum, a monotonic counter of time).
Labels are folded into the variable name, sorted for stability, e.g. "querytrace.statements{kind=select,operation=GET /users}", since expvar has no label dimensions.
func NewExpvarRecorder ¶
func NewExpvarRecorder(root *expvar.Map) *ExpvarRecorder
NewExpvarRecorder returns a recorder backed by root. Publish root once under a name of your choosing — typically expvar.NewMap("querytrace") — and reuse the recorder across traces; every trace's metrics accumulate into it.
var dbMetrics = querytrace.NewExpvarRecorder(expvar.NewMap("querytrace"))
// ... per request:
defer dbMetrics.Record(trace) // or trace.RecordMetrics(dbMetrics)
func (*ExpvarRecorder) Counter ¶
func (r *ExpvarRecorder) Counter(name string, value int64, labels ...Label)
Counter adds value to the integer variable named by name and labels.
func (*ExpvarRecorder) Duration ¶
func (r *ExpvarRecorder) Duration(name string, d time.Duration, labels ...Label)
Duration adds d, in seconds, to the float variable named by name and labels — a running total, since expvar cannot record a distribution.
func (*ExpvarRecorder) Record ¶
func (r *ExpvarRecorder) Record(trace *Trace)
Record emits trace's metrics into the expvar map, the one-call form of trace.RecordMetrics(r).
type LogExporter ¶
type LogExporter struct {
// contains filtered or unexported fields
}
LogExporter logs traces to an slog.Logger. It is the summary form of tracing output: at the end of a unit of work, log one record of the trace's counts and its warnings, rather than a line per query. For richer, span-based output use the separate otelquery module.
exporter := querytrace.NewLogExporter(logger) defer exporter.ExportTrace(ctx, trace)
func NewLogExporter ¶
func NewLogExporter(logger *slog.Logger, opts ...LogOption) *LogExporter
NewLogExporter returns a LogExporter that logs to logger.
func (*LogExporter) ExportTrace ¶
func (e *LogExporter) ExportTrace(ctx context.Context, trace *Trace)
ExportTrace logs a summary of the trace: its operation and counts at info level, raised to warn when it produced warnings, followed by one record per warning. It honors the configured LogMode.
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 Metrics ¶
type Metrics struct {
// Operation is the unit of work the trace covers, the natural metric label.
Operation string
// QueryCount is the number of read and write statements.
QueryCount int
// WriteCount is the number of write statements (INSERT/UPDATE/DELETE).
WriteCount int
// UniqueFingerprints is the number of distinct query shapes.
UniqueFingerprints int
// TransactionCount is the number of transactions begun.
TransactionCount 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
// ByStatement counts statements per kind (select/insert/update/delete/ddl).
ByStatement map[StmtKind]int
// Warnings counts warnings per code (n+1, query budget, select star, ...).
Warnings map[WarningCode]int
// WarningsBySeverity counts warnings per severity.
WarningsBySeverity map[Severity]int
}
Metrics is the numeric aggregate of a Trace, ready to emit to any metrics backend. It is a plain value — compute it with Trace.Metrics, inspect it, or push it through a MetricsRecorder with Emit. It deliberately depends on nothing but the standard library, so an application wires it to Prometheus, OpenTelemetry metrics, statsd, or its own counters by implementing the two-method MetricsRecorder; querytrace does not pull in an instrumentation library.
func MetricsFromReport ¶
MetricsFromReport derives Metrics from an already-built Report, for callers that produce the Report for other purposes too.
func (Metrics) Emit ¶
func (m Metrics) Emit(rec MetricsRecorder)
Emit pushes the metrics to rec under stable names (the Metric* constants), tagging each with the trace's operation when set. Statements are emitted per kind and warnings per code, so a backend can break them down.
type MetricsRecorder ¶
type MetricsRecorder interface {
Counter(name string, value int64, labels ...Label)
Duration(name string, d time.Duration, labels ...Label)
}
MetricsRecorder is the sink Metrics.Emit pushes to: an adapter over a real metrics backend. Counter adds a delta to a named counter and Duration records a duration observation (a histogram/summary sample), each tagged with zero or more labels. Implement it over prometheus.CounterVec/HistogramVec, an OpenTelemetry meter, or any other backend.
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.
func WithSampled ¶
WithSampled sets the Trace's sampling decision directly, for callers that have already decided whether to observe this unit of work. WithSampled(false) makes the wrapped driver a pass-through.
func WithSampler ¶
WithSampler decides at creation whether the Trace records, by calling s.ShouldSample once. A sampled-out Trace makes the wrapped driver a pass-through for its lifetime: it records no statements and reports nothing. Without this option (or WithSampled) a Trace always records.
type PredicateOption ¶
type PredicateOption func(*predicateConfig)
PredicateOption configures a tenant or soft-delete predicate check: the marker columns it requires (Columns) and, optionally, the schema scope that limits it to the tables that actually have one (ScopedToSchema). It is the argument type of WithTenantCheck and WithSoftDeleteCheck.
func Columns ¶
func Columns(specs ...string) PredicateOption
Columns sets the marker column specs a predicate check looks for in a statement's top-level WHERE, such as "tenant_id"/"org_id" for a tenant check or "deleted_at" for a soft-delete check. Each spec is a bare column or a table-qualified "table.column" (a leading schema is ignored), and both halves accept glob wildcards (* ? [..]), matched case-insensitively:
Columns("deleted_at") // any table, column deleted_at
Columns("deleted_at", "deactivated_at") // either marker, any table
Columns("*_at") // any table, any column ending in _at
Columns("orders.deleted_at") // only the orders table
Columns("*.deleted_at") // explicit any-table form
A bare column governs every table; a table-qualified spec restricts the requirement to the matching tables, so other tables are left unchecked. Columns is required: a check with no columns does nothing.
func ScopedToSchema ¶
func ScopedToSchema(md meta.Metadata) PredicateOption
ScopedToSchema scopes a predicate check to the base tables that, per the resolved schema md, actually have a marker column — eliminating the false positive the check otherwise raises on a table that has no such column (a lookup or reference table). Without it the check applies to every analyzed base table. Pass the meta.Metadata a decl catalog builds or the introspect produces. To scope by hand instead, name the tables in the Columns specs ("orders.deleted_at").
type QueryInfo ¶
type QueryInfo struct {
// Analysis is the static SQL analysis of the statement, produced by the analyze
// framework. Its fields are promoted onto QueryInfo.
analysis.Analysis
// RawSQL is the executed SQL text, kept only when raw SQL capture is on
// (WithRawSQL); otherwise empty.
RawSQL string
// ParamCount is the number of bind arguments.
ParamCount int
// ParamTypes are the Go type names of the bind arguments, kept when argument
// capture is ArgsTypesOnly or higher (WithArgCapture).
ParamTypes []string
// TxID links the statement to its transaction, empty outside one.
TxID string
}
QueryInfo holds the SQL details of an EventQuery or EventExec. It is split into two halves: the static analysis of the statement — the embedded analysis.Analysis, the facts derived from the SQL itself (kind, tables, clause shape, fingerprint), owned and produced by the analyze framework — and the runtime details querytrace captures around execution (raw text, parameters, transaction). The Analysis fields are promoted, so qi.Kind, qi.Tables, qi.HasTopLevelWhere, and the rest read directly. Raw SQL and argument values are kept only when configuration allows; the static fields 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 Sampler ¶
type Sampler interface {
// ShouldSample reports whether a new Trace should record. It is called once
// per Trace and may be probabilistic.
ShouldSample() bool
}
Sampler decides whether a Trace records the statements run under it. The decision is made once, when the Trace is created (see New and WithSampler), so a sampled-out Trace turns the wrapped driver into a thin pass-through for its whole lifetime — every statement under it is cheap, and the Trace records and reports nothing. Sampling keeps the per-statement overhead of analysis, fingerprinting, and caller capture off the hot path in production while still observing a representative fraction of traffic.
Sampling is a whole-Trace decision on purpose: a fractional, per-statement sampler would fragment a trace, leaving partial statement sets that make N+1 detection and query budgets meaningless. Either a Trace is observed in full or not at all.
func AlwaysSample ¶
func AlwaysSample() Sampler
AlwaysSample returns a Sampler that records every Trace. It is the default behavior when no sampler is set.
func NeverSample ¶
func NeverSample() Sampler
NeverSample returns a Sampler that records no Trace, turning the wrapped driver into a pass-through. It is useful to disable observation behind a flag without changing call sites.
func SampleFraction ¶
SampleFraction returns a Sampler that records each Trace independently with probability p. A p at or below 0 never samples and a p at or above 1 always does, so SampleFraction(0) and SampleFraction(1) match NeverSample and AlwaysSample. The decision uses the default math/rand source; seed it (or supply a custom Sampler) when a deterministic stream is needed.
type StmtKind ¶
StmtKind classifies a SQL statement by the kind of statement it represents. It is an alias for analysis.StmtKind: the statement analysis lives in the analyze package, but querytrace keeps the name so QueryInfo.Kind and NPlusOne.IgnoreOperations stay typed and the StmtXxx constants below read as part of the querytrace API.
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) RecordMetrics ¶
func (t *Trace) RecordMetrics(rec MetricsRecorder)
RecordMetrics computes the Trace's Metrics and emits them to rec. It is the one-call form of t.Metrics().Emit(rec), typically deferred alongside the unit of work the way otelquery.ExportTrace is.
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) Sampled ¶
Sampled reports whether the Trace records the statements run under it. It is fixed when the Trace is created (see WithSampler / WithSampled) and defaults to true. A wrapped driver consults it to skip recording for a sampled-out Trace.
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" // WarnLargeInList marks a statement whose top-level WHERE carries an IN (...) // value list at or above the configured length, a shape that strains the // planner and the bind-parameter limit and is usually better expressed as a // join or a temporary table. WarnLargeInList WarningCode = "large_in_list" // WarnSelectStar marks a SELECT that projects every column with "*", which // over-fetches, couples the caller to the table's column set, and defeats // covering indexes. WarnSelectStar WarningCode = "select_star" // WarnMissingTenantPredicate marks a read or write that touches a base table // without referencing any of the required tenant-scoping columns in its // top-level WHERE, the signature of a query that can cross tenant boundaries. WarnMissingTenantPredicate WarningCode = "missing_tenant_predicate" // WarnMissingSoftDeletePredicate marks a SELECT that touches a base table // without referencing a soft-delete marker column in its top-level WHERE, so // it may return logically deleted rows. WarnMissingSoftDeletePredicate WarningCode = "missing_soft_delete_predicate" // WarnEqNull marks a comparison against a NULL literal with = or <> // (x = NULL / x <> NULL), which SQL evaluates to unknown rather than matching // NULL, so the predicate silently drops every row; IS NULL / IS NOT NULL is // meant. WarnEqNull WarningCode = "eq_null" // WarnWriteOutsideTransaction marks two or more writes issued outside any // transaction within one Trace — statements that cannot roll back together if // one fails, a consistency risk a single autocommit write does not carry. WarnWriteOutsideTransaction WarningCode = "write_outside_transaction" // WarnExternalCallInTransaction marks a Mark (an application step such as an // external API call) recorded while a transaction was open: work that holds the // transaction — and its locks and pinned connection — open across a // non-database operation. WarnExternalCallInTransaction WarningCode = "external_call_in_transaction" )
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. |