xetrace

package
v0.1.49 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package xetrace manages short-lived SQL Server Extended Events sessions backed by a ring_buffer or event_file target, and attaches to the built-in system_health session. Transport and session registries belong to callers.

Index

Constants

View Source
const (
	EventSQLStatementCompleted = "sql_statement_completed"
	EventRPCCompleted          = "rpc_completed"
	EventSQLBatchCompleted     = "sql_batch_completed"
	EventErrorReported         = "error_reported"
	// EventSPStatementCompleted fires once per statement executed INSIDE a
	// stored procedure — the only way to see a procedure's internals, which
	// rpc_completed reports as a single aggregate. It is opt-in (absent from
	// DefaultEvents): a busy instance emits it for every statement of every
	// proc, which displaces the calls the user asked for in a ring buffer
	// capped at MaxEvents.
	EventSPStatementCompleted = "sp_statement_completed"
	// The object events report one committed schema change each — the object's
	// name, type and database — whatever statement or batch made it. They are
	// opt-in through Events.
	EventObjectCreated     = "object_created"
	EventObjectAltered     = "object_altered"
	EventObjectDeleted     = "object_deleted"
	EventXMLDeadlockReport = "xml_deadlock_report"
)

Event names supported by CreateOptions.Events.

View Source
const AllDatabases = "*"

AllDatabases is the CreateOptions.Databases value that captures every database on the instance.

View Source
const AutoPath = "auto"

AutoPath is the FileTarget.Path value that asks Create to resolve a writable directory on the SQL Server host itself. It is the normal way to select the event_file target: a client-side guess is wrong on RDS, where the instance may only write to rdsEventFileDir, and wrong on any instance whose data volume layout we do not know.

View Source
const DispatchLatency = 1 * time.Second

DispatchLatency is the MAX_DISPATCH_LATENCY applied to every session's target. SQL Server buffers events internally and only flushes them to the target after this window elapses (or when a buffer fills), so a caller that stops a session MUST wait out this latency to capture a span shorter than it — otherwise the final poll reads an empty buffer. 1 second is SQL Server's documented minimum for a ring_buffer target.

View Source
const SystemHealthSession = "system_health"

SystemHealthSession is the built-in session CreateOptions.Session can attach to instead of creating one.

Variables

DefaultEvents is the set captured when CreateOptions.Events is empty. It is deliberately NOT every supported event: EventSPStatementCompleted is opt-in and must be named explicitly (`--event`), because capturing it changes both the volume and the shape of every trace; the ObjectEvents are opt-in too.

View Source
var ErrSessionGone = errors.New("event session is no longer present in sys.dm_xe_sessions (dropped externally, or the instance restarted)")

ErrSessionGone reports that the XE session vanished from sys.dm_xe_sessions mid-capture. The DMV row (and its ring_buffer target row, with eventCount="0") exists from the moment ALTER … STATE = START returns, so its absence never means "not ready yet" — the session was dropped by another admin, or the instance restarted or failed over. Retrying can never produce data, so this is deliberately NOT a transient poll error.

FilterableTypes is every token an EventFilter.Types entry may name: each StatementType plus the virtual DML group. It is the option set a UI offers, derived from the same constants typeTokens emits so a picker can never drift from what the filter actually matches.

ObjectEvents are the schema-change events callers can select for capture.

SupportedEvents is every XE event name CreateOptions.Events accepts. A name outside this set is rejected by NormalizeEvents rather than emitted into DDL that SQL Server refuses at CREATE time.

Functions

func BuildCreateSQL

func BuildCreateSQL(opts CreateOptions) (string, error)

BuildCreateSQL assembles the CREATE EVENT SESSION DDL for the given options. Exported for testing.

func CurrentDatabase

func CurrentDatabase(ctx context.Context, db *sql.Conn) (string, error)

CurrentDatabase returns DB_NAME() for the current connection.

func CurrentSessionID

func CurrentSessionID(ctx context.Context, db *sql.Conn) (int, error)

CurrentSessionID returns the sqlserver session_id of the current connection, so callers can pass it as CreateOptions.ExcludeSessionID.

func DefaultRingBufferMemoryKB

func DefaultRingBufferMemoryKB() int

DefaultRingBufferMemoryKB is the ring buffer's size cap in KB. Exported for the same reason as PollTimeout: a caller that has to stay bigger than one poll's worth of events needs to size against it rather than hardcode a number that silently becomes too small when this default moves.

func Drain

func Drain(ctx context.Context, p poller, opts DrainOptions) error

Drain polls p at the configured interval, deduplicates events via Event.Key, and delivers each new event to opts.OnEvent. It keeps running until ctx is cancelled, then performs a final drain so late-arriving events captured right before cancellation are not lost.

Two pieces of cross-poll state ride along, because both span a boundary a single poll cannot see:

  • a HandleCache, so an sp_execute prepared in one poll still resolves to its statement text when executed in the next;
  • a streamNester, so a procedure's inner statements are delivered after the call that ran them rather than before it.

Both survive a tolerated poll failure, which is the main reason the retry lives here rather than around Session.Poll or around Drain itself: restarting the loop would discard them and re-deliver the whole ring buffer as new.

The drain poll uses a bounded background context (not ctx) so shutdown still completes when the caller's context is already Done.

func DropTimeout

func DropTimeout() time.Duration

DropTimeout is the fixed budget Session.Drop gives itself. Exported for the same reason as PollTimeout: a caller waiting on a drain has to cover both.

func HasChildren

func HasChildren(events []Event) bool

HasChildren reports whether any event in the slice carries nested inner statements, i.e. whether Nest found anything to attach. Callers use it to decide between flat table and tree rendering.

func IsTransientPollError

func IsTransientPollError(err error) bool

IsTransientPollError reports whether a ring_buffer read failure is worth re-attempting on a fresh connection.

It is an ALLOWLIST: an error this function does not recognise is terminal, so a genuine failure (a revoked permission, a dropped session, a malformed payload) surfaces immediately with its own first message instead of being retried N times and reported late behind a retry-count wrapper.

The canonical member is mssql.StreamError. When a poll's deadline fires mid-read the driver sends a TDS attention packet and waits for the server to confirm the cancellation; a server still materialising target_data has not produced a first byte and does not answer in time, which yields "Invalid TDS stream: did not get cancellation confirmation from the server". The driver marks that connection bad and database/sql discards it, so the next attempt is issued on a healthy connection — which is exactly why retrying works.

Pinned connections can surface ErrBadConn or ErrConnDone instead of being transparently replaced by database/sql. ErrSessionGone remains terminal.

func NormalizeEvents

func NormalizeEvents(names []string) ([]string, error)

NormalizeEvents resolves a caller-supplied event MultiFilter against SupportedEvents with collections.MatchItems semantics: a plain name must be supported, a `*` / prefix / suffix pattern selects the supported events it matches, exclusions win, and an exclusion-only list is DefaultEvents minus the exclusions (e.g. `!error_reported`). The result is a de-duplicated, concrete, lower-case list; one that selects nothing is an error. An empty input returns nil, leaving Create to apply DefaultEvents.

func PollTimeout

func PollTimeout() time.Duration

PollTimeout is the per-poll ring_buffer read deadline. Exported because the registry sizes its stop-wait budget from it.

func ResolveEventFilePath added in v0.1.42

func ResolveEventFilePath(ctx context.Context, db *sql.Conn, file FileTarget, sessionName string) (string, error)

ResolveEventFilePath turns an AutoPath request into a concrete file on the server — <log directory><sep><session name>.xel — and validates an explicit one. The session name keeps two concurrent captures off each other's files.

func StreamLine added in v0.1.41

func StreamLine(e Event, opts StreamLineOptions) api.Text

StreamLine builds a styled single-line representation of an event for live streaming to stderr during the poll loop. The final summary table is built separately via Event.Columns/Row — this function is only for the per-event live stream.

func SystemHealthFilePattern added in v0.1.42

func SystemHealthFilePattern(currentFile string) (string, error)

SystemHealthFilePattern is the glob matching every rollover file of the system_health session whose current file is currentFile.

func UnwrapRPC

func UnwrapRPC(raw string) (unwrapped string, ok bool)

UnwrapRPC rewrites a SQL Server RPC invocation into the inner statement it is actually executing, with parameter placeholders substituted by their literal values. Three shapes are recognized:

  1. sp_prepexec — wrapped in a `declare @p1 int set @p1=N exec sp_prepexec @p1 output, N'@P0 type', N'TEMPLATE', v0, v1, … select @p1` scaffold.
  2. sp_executesql — `[exec] sp_executesql N'TEMPLATE', N'@P0 type', v0, v1, …`.
  3. Positional CALL — `{call proc(?, ?, ?)}` or `call proc(?, ?, ?)` with values supplied as additional ordered args (we don't see those at this layer, so positional CALL is returned untouched with `?` placeholders).

When no rewrite applies the original input is returned verbatim along with ok=false, so callers can decide whether to fall back to raw rendering.

func ValidateEventFilePath added in v0.1.42

func ValidateEventFilePath(path string) error

ValidateEventFilePath checks a caller-supplied event_file path offline, so a bad one fails when the args are parsed rather than at CREATE EVENT SESSION. AutoPath passes: it is resolved against the server in Create.

func ValidateMatchPatterns added in v0.1.42

func ValidateMatchPatterns(values []string) error

ValidateMatchPatterns refuses a MultiFilter value the XE session predicate cannot honour exactly as collections.MatchItems would: a `*` anywhere but the start or end (MatchItems reads it literally, a LIKE would not), an exclusion naming nothing, an exclusion of everything, or a malformed URL escape.

Types

type Accumulator added in v0.1.42

type Accumulator struct {
	// contains filtered or unexported fields
}

Accumulator folds events into a Summary. It exists so the live server path can maintain a running total per poll instead of re-reading and re-summing the whole capture on every request.

Not safe for concurrent use; the caller owns the locking.

func NewAccumulator added in v0.1.42

func NewAccumulator() *Accumulator

func (*Accumulator) Add added in v0.1.42

func (a *Accumulator) Add(e Event)

Add folds one event, in emission order. Attribution of inner statements relies on streamNester having already emitted a parent ahead of the statements it ran, which is exactly what Drain delivers. Use AddAll for a slice whose order is not guaranteed.

func (*Accumulator) AddAll added in v0.1.42

func (a *Accumulator) AddAll(events []Event)

AddAll folds a whole capture. It pre-registers every parent first, so attribution does not depend on the input being in emission order.

func (*Accumulator) AddLost added in v0.1.42

func (a *Accumulator) AddLost(n int64)

AddLost records events the capture never read (see Summary.Lost). It takes the additive deltas DrainOptions.OnDropped reports.

func (*Accumulator) AddUnresolved added in v0.1.42

func (a *Accumulator) AddUnresolved()

AddUnresolved records one event DrainOptions.OnUnresolved reported.

func (*Accumulator) Result added in v0.1.42

func (a *Accumulator) Result() Summary

Result renders the running totals, deriving the figures that can only be computed once every event is in (averages, percentile, window).

type CreateOptions

type CreateOptions struct {
	// Session attaches to a built-in session instead of creating one. The only
	// supported value is system_health.
	Session string
	// Name of the XE session. Required: there is no default.
	//
	// An Extended Events session is a server-scoped object — it shows up in
	// sys.dm_xe_sessions for every DBA on the instance, alongside sessions
	// created by anything else. A name this package invented would tell them
	// which library made it and nothing about which application, so the name
	// belongs to the caller who can answer that.
	Name string
	// Databases scopes the session by database name using clicky MultiFilter
	// values with collections.MatchItems semantics (case-insensitive exact,
	// prefix/suffix `*`, `!` exclusion, comma lists). Translated into the XE
	// WHERE predicate, as are Users, Apps and Hosts, so the filter runs in SQL
	// Server before the target, not post-capture in Go.
	//
	// Empty scopes to the connection's own database, which Create resolves with
	// DB_NAME(); AllDatabases ("*") captures the whole instance. Instance-wide
	// is the wider, more surprising scope, so it is the one that has to be
	// asked for.
	Databases []string
	// Users scopes the session by SQL login with the same patterns as
	// Databases. Empty captures all users.
	Users []string
	// MinDurationMicros filters out events faster than this threshold.
	// Only applied to duration-bearing events (statement/rpc/batch).
	MinDurationMicros int64
	// Events is the list of XE event names to capture. When empty,
	// DefaultEvents is used.
	Events []string
	// ExcludeSessionID is populated by Create from its dedicated connection.
	ExcludeSessionID int
	// Apps scopes the session by client application name with the same
	// patterns as Databases. Empty captures every application.
	Apps []string
	// Hosts scopes the session by client host name with the same patterns as
	// Databases. Empty captures every host.
	Hosts []string
	// MaxMemoryKB is the ring buffer size. Zero uses the
	// sqltrace.ringBuffer.maxMemoryKb property. Meaningless with File set.
	MaxMemoryKB int
	// MaxEvents caps the ring buffer event count. Zero uses the
	// sqltrace.ringBuffer.maxEvents property. Meaningless with File set.
	MaxEvents int
	// File selects a package0.event_file target instead of the ring buffer:
	// events are written to .xel files on the SQL Server host and read back
	// incrementally, so a high-volume capture cannot lose events to the ring
	// buffer's FIFO eviction. Nil keeps the ring buffer.
	File *FileTarget
	// Filter narrows captured events by statement type / referenced table. XE
	// carries no structured type/object info to predicate on at the session
	// level, so it is recorded here with the rest of the capture's configuration
	// and applied by Drain — pass it as DrainOptions.Filter — which is the first
	// point an sp_execute has been resolved to the statement it re-ran. Zero
	// value matches everything. It does not change the XE events captured.
	Filter EventFilter
}

CreateOptions configures a new Extended Events session.

func (CreateOptions) DrainFilter added in v0.1.42

func (o CreateOptions) DrainFilter() EventFilter

DrainFilter is Filter as Drain applies it. A deadlock report is added to the session without a predicate, and system_health's events never pass one of ours, so for a capture that reads either the filter is scoped to Databases, unless it names databases of its own.

type DrainOptions

type DrainOptions struct {
	// Interval is the ring-buffer poll cadence. Defaults to one second.
	Interval time.Duration
	// StartedAt bounds an attached session, whose target holds events from
	// before the capture and keeps receiving them after it: events stamped
	// before StartedAt, or at or after the moment ctx ends, are not delivered.
	// Leave it zero for a session this process created, which only ever holds
	// the capture's own events — comparing the server's timestamps with this
	// host's clock would drop the last ones whenever the server's runs ahead.
	StartedAt time.Time
	// FinalDelay is how long Drain waits after ctx ends before its final read
	// (Session.FinalDelay).
	FinalDelay time.Duration
	// OnEvent receives each new, deduplicated event in delivery order. It is
	// invoked synchronously while dedup state is held — keep it fast (an append
	// or a channel send, never a network round-trip).
	OnEvent func(Event)
	// OnPollBatch fires once after each poll's events have all been delivered,
	// marking a natural batch boundary for a caller that persists in chunks.
	// It does NOT fire for a poll that failed.
	OnPollBatch func()
	// OnPollFailure reports a transient poll failure that is being retried,
	// with the number of consecutive failures so far. xetrace has no logger of
	// its own (DB chatter flows through gormlog), so callers do the logging.
	OnPollFailure func(consecutive int, err error)
	// OnDropped reports that the server dispatched more events to the ring
	// buffer than we read back, or that it truncated/dropped events itself.
	// This is how a tolerated poll failure stays auditable instead of silently
	// losing the events the buffer evicted while we were not reading it.
	//
	// Every delta is additive — the events lost since the previous report — so
	// a caller can sum them into a capture's total loss. A truncated target is
	// reported even when it carries no count of its own.
	OnDropped func(delta int64, stats TargetStats)
	// Filter narrows delivery by every supported event dimension. It is
	// applied here rather than as each ring-buffer read is parsed because only
	// here has an sp_execute been resolved to the statement it re-ran: before
	// that, every re-run of a prepared statement reads as Tables=[sp_execute]
	// and a table pattern would drop it. The zero value delivers everything.
	Filter EventFilter
	// OnUnresolved receives each event Filter excluded only because its text is
	// unknowable — an sp_execute of a handle prepared before the capture
	// started. Its cost is real but cannot be placed, so a caller that sums the
	// delivered events must count these rather than let them vanish.
	OnUnresolved func(Event)
}

DrainOptions configures the poll loop. Every field is optional; zero values fall back to the package defaults in properties.go.

type Event

type Event struct {
	Name              string         `json:"name"`
	Timestamp         time.Time      `json:"timestamp"`
	Duration          time.Duration  `json:"duration"`
	CPUTime           time.Duration  `json:"cpu_time"`
	LogicalReads      int64          `json:"logical_reads"`
	PhysicalReads     int64          `json:"physical_reads"`
	Writes            int64          `json:"writes"`
	RowCount          int64          `json:"row_count"`
	DatabaseName      string         `json:"database_name"`
	ClientApp         string         `json:"client_app_name"`
	ClientHost        string         `json:"client_hostname"`
	Username          string         `json:"username"`
	SessionID         int            `json:"session_id"`
	Statement         string         `json:"raw_statement,omitempty"`
	SQL               string         `json:"statement"`
	StatementType     StatementType  `json:"statement_type,omitempty"`
	Tables            []string       `json:"tables,omitempty"`
	ErrorNumber       int            `json:"error_number,omitempty"`
	ErrorMessage      string         `json:"error_message,omitempty"`
	AdditionalFields  map[string]any `json:"additional_fields,omitempty"`
	DeadlockReportXML string         `json:"deadlock_report_xml,omitempty"`
	DeadlockDatabases []string       `json:"deadlock_databases,omitempty"`

	// ObjectName is the procedure an event ran inside, captured whenever the
	// event exposes it — rpc_completed always does; sp_statement_completed
	// reports object_id instead on some server versions, which is kept in
	// ObjectID. Neither is the attribution mechanism for inner statements:
	// that is Nest, keyed on ActivityID. These are for display only.
	ObjectName string `json:"object_name,omitempty"`
	ObjectID   int64  `json:"object_id,omitempty"`

	// ObjectType is what an object event created, altered or deleted, as SQL
	// Server's object_type map names it: USRTAB, INDEX, PROC, VIEW, …. Empty
	// for every other event.
	ObjectType string `json:"object_type,omitempty"`

	// ActivityID and ActivitySeq come from package0.attach_activity_id, which
	// is only attached when TRACK_CAUSALITY is on (see wantsCausality). Every
	// event raised while servicing one request shares an ActivityID, with
	// ActivitySeq increasing in completion order — this is what lets Nest put
	// a procedure's inner statements under the call that ran them.
	ActivityID  string `json:"activity_id,omitempty"`
	ActivitySeq int    `json:"activity_seq,omitempty"`

	// Sequence is package0.event_sequence: the session's running number for this
	// event, unique within one capture. It is what keys an event, because two
	// distinct events can agree on every other field — a connection reset raises
	// 5701 and 5703 in the same millisecond on one session, with no duration and
	// no statement.
	Sequence int64 `json:"sequence,omitempty"`

	// Children are the inner statements attributed to this event by Nest.
	// Always empty until Nest runs.
	Children []Event `json:"children,omitempty"`

	// ParamsUnavailable marks a statement whose parameter values are not
	// recoverable from the trace — a positional `{call p(?,?)}` that the
	// driver did not expand, or an sp_execute whose prepared handle was
	// established before capture started. The placeholders are shown as
	// captured; this flag stops them being read as the literal call.
	ParamsUnavailable bool `json:"params_unavailable,omitempty"`
}

Event is a decoded row from a ring_buffer target.

func Nest

func Nest(events []Event) []Event

Nest groups events by their causality activity id and attaches each group's inner statements to the call that ran them, returning the parents (and every ungrouped event) in the original order.

Inner statements COMPLETE BEFORE the call containing them, so a flat trace lists a procedure's body ahead of the procedure itself. Nest reverses that into the containment the reader expects.

It is deliberately conservative — anything it cannot attribute is passed through unchanged rather than guessed at:

  • an event with no ActivityID (causality off, i.e. every trace that does not opt into sp_statement_completed) stays exactly where it was;
  • a group with no parent event — the parent fell outside the capture window, or was dropped by a --min-duration/--table filter — stays flat, so its statements are still visible rather than silently swallowed;
  • a group with several parents keeps the last one as the owner and leaves the others as top-level rows, since nothing in the activity id says which nests inside which.

Children are ordered by ActivitySeq (completion order within the request). Nest does not mutate its input.

func (Event) Columns added in v0.1.41

func (Event) Columns() []api.ColumnDef

Columns implements api.TableProvider on Event.

func (Event) Key

func (e Event) Key() string

Key returns a string uniquely identifying this event within a ring buffer. Used by callers to deduplicate across overlapping polls. Sequence is what makes it unique; the other fields only describe the event, and without the sequence two events that agree on all of them collapse into one.

func (Event) MergedStatement

func (e Event) MergedStatement() string

MergedStatement returns the SQL with parameters inlined. For RPC events (sp_prepexec, sp_executesql) this unwraps the scaffold and substitutes @P0/@P1/… with their literal values. For plain statements it returns the whitespace-collapsed original.

func (Event) Row added in v0.1.41

func (e Event) Row() map[string]any

Row implements api.TableProvider on Event.

func (Event) RowDetail added in v0.1.41

func (e Event) RowDetail() api.Textable

RowDetail implements api.DetailProvider: the expanded row shows the full statement (as a syntax-highlighted SQL block) plus metadata and — for error events — the full error payload.

type EventFilter

type EventFilter struct {
	Events      []string
	Databases   []string
	Users       []string
	Apps        []string
	Hosts       []string
	Types       []string
	Tables      []string
	MinDuration time.Duration
}

EventFilter narrows captured events by statement type and referenced table, using collections.MatchItems semantics (case-insensitive, `*` wildcards, `!` exclusion). Both lists are matched against the event's STRUCTURED tokens (typeTokens / Tables), never the raw SQL — so no LIKE/regex is involved.

type FileTarget added in v0.1.42

type FileTarget struct {
	// Path is where SQL Server writes the files, on ITS OWN filesystem. AutoPath
	// resolves the instance's log directory (see ResolveEventFilePath); any
	// other value is used exactly as given and must be an absolute path ending
	// in .xel.
	Path string
	// MaxFileSizeMB caps one file before SQL Server rolls over. Zero uses the
	// sqltrace.eventFile.maxFileSizeMb property.
	MaxFileSizeMB int
	// MaxRolloverFiles caps how many rolled files are kept. Zero uses the
	// sqltrace.eventFile.maxRolloverFiles property. Together with MaxFileSizeMB
	// this is the capture's whole disk budget on the server.
	MaxRolloverFiles int
}

FileTarget configures a package0.event_file target: events are written to .xel files on the SQL Server host and read back incrementally with sys.fn_xe_file_target_read_file. Unlike the ring buffer it never evicts, so a high-volume capture keeps every event at the cost of disk on the server.

type HandleCache

type HandleCache struct {
	// contains filtered or unexported fields
}

HandleCache resolves sp_execute calls back into the statement their sp_prepexec prepared. It is stateful across polls by necessity: a handle can be prepared in one ring-buffer read and executed in the next.

A handle is scoped to the connection that prepared it, and concurrent connections reuse the same numbers, so the cache is keyed by (session, handle): an sp_execute only ever resolves to its own session's prepare.

Not safe for concurrent use — Drain owns one per capture and calls it from a single goroutine.

func NewHandleCache

func NewHandleCache() *HandleCache

NewHandleCache returns an empty cache.

func (*HandleCache) Observe

func (c *HandleCache) Observe(e Event)

Observe records the SQL template an sp_prepexec event prepared, keyed by its session and handle. Events that are not sp_prepexec are ignored.

func (*HandleCache) Resolve

func (c *HandleCache) Resolve(e *Event) (unresolved bool)

Resolve rewrites an sp_execute event in place into the call it re-ran, substituting the event's own argument values into the cached template. When the handle is unknown on the event's session — prepared before capture started — the values are kept and the missing text is named explicitly, with ParamsUnavailable set, and Resolve reports it: no filter on type or table can place such a statement.

Events that are not sp_execute are left alone.

type PermissionError

type PermissionError struct {
	Report PermissionReport
}

PermissionError reports an actionable XEvent permission failure.

func (*PermissionError) Error

func (e *PermissionError) Error() string

type PermissionReport

type PermissionReport struct {
	Login               string   `json:"login"`
	ProductMajorVersion int      `json:"productMajorVersion"`
	Granted             bool     `json:"granted"`
	MissingPermissions  []string `json:"missingPermissions,omitempty"`
	GrantStatements     []string `json:"grantStatements,omitempty"`
}

PermissionReport describes whether the connected SQL Server login can run the complete server-scoped XEvent lifecycle used by this package.

func CheckPermissions

func CheckPermissions(ctx context.Context, db *sql.Conn) (PermissionReport, error)

CheckPermissions probes the permissions required by Create, Poll, and Drop.

type Querier added in v0.1.42

type Querier interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

Querier runs a query: a pool, or one pinned connection whose session settings — its current database, say — the replay must run under.

type RPCCall

type RPCCall struct {
	Template  string
	ParamDecl string
	Values    []string
}

RPCCall retains a prepared statement's template independently of its values.

func ParseRPC

func ParseRPC(raw string) (*RPCCall, bool)

ParseRPC decodes the parameter-bearing RPC forms captured by SQL Server.

func (*RPCCall) ToQuery added in v0.1.41

func (c *RPCCall) ToQuery() (string, []any, []coerceWarning)

ToQuery returns the replayable form of the call: a SQL string with ordered @p1..@pN bind markers and the positional args, type-coerced when a declaration provides enough info. When decl is missing, values are passed as strings and the driver does its best.

SQL Server accepts only @pN bind markers, so the markers are written here rather than left as `?` for a driver to translate.

type ReplayResult added in v0.1.41

type ReplayResult struct {
	// EventKey links back to Event.Key() for correlation with the
	// original trace row.
	EventKey string `json:"event_key"`
	// OriginalStatement is the raw statement text as it came out of the
	// trace, kept for display so users can compare what was captured with
	// what was actually replayed.
	OriginalStatement string `json:"original_statement"`
	// Eligible indicates whether the replay actually ran. When false,
	// SkipReason explains why (non-SELECT, INTO/EXEC present, error
	// event, etc.).
	Eligible   bool   `json:"eligible"`
	SkipReason string `json:"skip_reason,omitempty"`
	// SQL and Args are the parameterized form passed to the driver —
	// useful for users who want to re-run the query manually.
	SQL  string `json:"sql,omitempty"`
	Args []any  `json:"args,omitempty"`
	// RowCount is the exact number of rows the replay returned. When
	// RowCountTruncated is true the replay hit the scan cap and the
	// count is a lower bound.
	RowCount          int  `json:"row_count"`
	RowCountTruncated bool `json:"row_count_truncated,omitempty"`
	// FirstRows holds up to 3 column-name-keyed maps for display.
	FirstRows []map[string]any `json:"first_rows,omitempty"`
	// Warnings from type coercion (e.g. unknown SQL type, unparseable
	// datetime) so the user can see where fidelity may have been lost.
	Warnings []coerceWarning `json:"warnings,omitempty"`
	// Error is the driver or scan error, if any. Non-empty means the
	// replay ran but failed partway; Eligible is still true in that case.
	Error string `json:"error,omitempty"`
	// Duration is the wall time spent on this replay.
	Duration time.Duration `json:"duration"`
}

ReplayResult is one entry in TraceResult.Replays: the outcome of replaying a single captured event. Serializable as JSON so --format json surfaces it alongside the raw trace data.

func Replay added in v0.1.41

func Replay(ctx context.Context, db Querier, events []Event) []ReplayResult

Replay walks the trace events, picks the ones that are safe to re-run, and executes each against db. The returned slice has one entry per input event (including skipped ones) so the caller can display both the replay results and why some were not replayed.

func ReplayOne added in v0.1.41

func ReplayOne(ctx context.Context, db Querier, e Event) ReplayResult

ReplayOne replays a single event and returns its result. Exported so the CLI drain loop can run replay inline immediately after streaming the original trace line, keeping replay output interleaved with the rest of the live output.

func (ReplayResult) Pretty added in v0.1.41

func (r ReplayResult) Pretty() api.Text

Pretty renders a single replay result as a compact block: the (truncated) SQL on the first line, row count + duration on the second, and up to 3 first rows as `key=value` lines below.

type Session

type Session struct {
	Name string
	// Statements are the CREATE EVENT SESSION and START statements Create
	// ran, in order, exactly as it executed them.
	Statements []string
	// FilePath is where an event_file session writes, as resolved on the
	// server. Empty for a ring_buffer session.
	FilePath string
	// contains filtered or unexported fields
}

Session represents a live XE session this process reads: one it created, or the built-in system_health session it attached to.

func AttachSystemHealth added in v0.1.42

func AttachSystemHealth(ctx context.Context, pool *sql.DB, opts CreateOptions) (_ *Session, err error)

AttachSystemHealth reads the built-in system_health session from the end of its current file onwards, so a capture sees only what happens after it starts. The session is not this process's: Drop releases the reader and leaves the session running.

func Create

func Create(ctx context.Context, pool *sql.DB, opts CreateOptions) (_ *Session, err error)

Create builds an XE session with a ring_buffer or event_file target, starts it, and returns a Session handle. Callers MUST defer s.Drop to avoid leaking the session on the server.

func (*Session) DispatchLatency added in v0.1.42

func (s *Session) DispatchLatency() time.Duration

DispatchLatency is the maximum time a final event can remain buffered before the session's target receives it.

func (*Session) DrainFilter added in v0.1.42

func (s *Session) DrainFilter() EventFilter

DrainFilter is the filter to pass as DrainOptions.Filter: the session's CreateOptions.Filter, scoped to the databases Create resolved for the events the session predicate cannot scope.

func (*Session) Drop

func (s *Session) Drop(parent context.Context) error

Drop stops and removes the session. Safe to call on a nil receiver. Uses a fresh timeout-bounded context so it still runs during shutdown when the caller context has already been cancelled. A session this process attached to rather than created is only released, never dropped.

func (*Session) FinalDelay added in v0.1.42

func (s *Session) FinalDelay() time.Duration

FinalDelay is how long a caller waits after stopping before the final read, so events that completed just before the stop have left SQL Server's dispatch buffer for the target. Pass it as DrainOptions.FinalDelay.

func (*Session) Poll

func (s *Session) Poll(ctx context.Context) (TargetSnapshot, error)

Poll reads the session's target — the whole ring buffer, or the .xel rows an event_file target has flushed since the previous poll — and returns the parsed events plus the target's own bookkeeping. Callers are responsible for deduplication via Event.Key across polls: both targets can hand the same event to two consecutive polls.

type StatementType

type StatementType string

StatementType is the coarse classification of a captured statement. It is derived from the top-level keywords of the merged SQL — NOT a substring scan — so the SQL text never has to be string-matched by callers filtering a trace. A statement opening with CREATE/ALTER/DROP/TRUNCATE is StmtDDL; otherwise the first top-level DML verb decides, and anything with none is StmtOther.

const (
	StmtSelect StatementType = "SELECT"
	StmtInsert StatementType = "INSERT"
	StmtUpdate StatementType = "UPDATE"
	StmtDelete StatementType = "DELETE"
	StmtMerge  StatementType = "MERGE"
	StmtExec   StatementType = "EXEC"
	StmtDDL    StatementType = "DDL"
	StmtOther  StatementType = "OTHER"
)

type StreamLineOptions added in v0.1.41

type StreamLineOptions struct {
	// ShowDatabase adds a "db=<name>" segment. Only set when the capture can
	// see more than one database (anything but exactly one plain --database);
	// a single-database capture would otherwise repeat the same value on every
	// line.
	ShowDatabase bool
	// Full removes the max-w-[200ch] cap on the statement cell so long
	// statements wrap naturally instead of being elided.
	Full bool
}

StreamLineOptions controls how StreamLine formats a single live event.

type Summary added in v0.1.42

type Summary struct {
	Events int `json:"events"`
	Errors int `json:"errors,omitempty"`
	// Nested is the count of inner statements whose metrics a parent event
	// already accounts for. Non-zero only when sp_statement_completed is traced.
	Nested int `json:"nested,omitempty"`
	// Window is wall-clock first event → last event, which is what the capture
	// actually spans — not the sum of the durations, which overlaps across
	// concurrent sessions.
	Window        time.Duration `json:"window"`
	Duration      time.Duration `json:"duration"`
	AvgDuration   time.Duration `json:"avg_duration"`
	P95Duration   time.Duration `json:"p95_duration"`
	MaxDuration   time.Duration `json:"max_duration"`
	CPUTime       time.Duration `json:"cpu_time"`
	LogicalReads  int64         `json:"logical_reads"`
	PhysicalReads int64         `json:"physical_reads"`
	// ReadBytes is LogicalReads × 8 KB — every page the capture read. A
	// physical read is a page SQL Server first fetched from disk, and that page
	// is then counted as a logical read too, so adding the two would count
	// every disk read twice.
	ReadBytes int64 `json:"read_bytes"`
	Writes    int64 `json:"writes"`
	RowCount  int64 `json:"row_count"`
	// Lost counts events SQL Server raised that the capture never read: evicted
	// from the ring buffer before a poll reached them, or dropped by the server.
	// Every figure above undercounts while it is non-zero.
	Lost int64 `json:"lost,omitempty"`
	// Unresolved counts re-runs of a statement prepared before the capture
	// started, which a type/table filter could not place: their text is
	// unknowable, so their cost is in no figure above either.
	Unresolved int64 `json:"unresolved,omitempty"`
}

Summary is the aggregate over one capture: what it cost in time, CPU and IO.

The sums cover TOP-LEVEL events only. When sp_statement_completed is traced, causality is on and an rpc_completed's duration/CPU/reads already include every inner statement it ran, so adding both would count the same work twice. Inner statements are reported separately as Nested. See Accumulator.isInner.

func Summarize added in v0.1.42

func Summarize(events []Event) Summary

Summarize folds a complete slice of events in one call.

func SummarizeMatching added in v0.1.42

func SummarizeMatching(events []Event, pattern string) Summary

SummarizeMatching folds only the top-level events whose statement text contains pattern — case-insensitive, with runs of whitespace collapsed on both sides so a pattern written on one line matches SQL a builder emitted across several. The text is the resolved SQL (what a prepared re-run actually ran), falling back to the raw statement when nothing was resolved. A matching event's inner statements come along as its own, counted but not summed again, exactly as Summarize treats them.

Loss is not attributable to a pattern — an event the capture never read has no text to match — so Lost and Unresolved stay zero here; a caller reporting a subset must carry the whole capture's.

func (Summary) CPURatio added in v0.1.42

func (s Summary) CPURatio() float64

CPURatio is CPU time as a fraction of elapsed time, or 0 when nothing ran. Above 1 means the capture ran work in parallel.

func (Summary) Pretty added in v0.1.42

func (s Summary) Pretty() api.Text

Pretty renders the summary as the cost line that follows the "N events captured" header, e.g.

12.4s elapsed · 8.1s cpu (65%) · 4.2M reads (32 GB) · 12K physical · 1.9K writes · 220 rows · avg 9.7ms p95 84ms max 1.2s

type SystemHealthSource added in v0.1.42

type SystemHealthSource struct {
	CurrentFile     string        `json:"currentFile"`
	FilePattern     string        `json:"filePattern"`
	DispatchLatency time.Duration `json:"dispatchLatency"`
}

SystemHealthSource is where the built-in system_health session writes and how long it may hold an event before its file sees it.

func ResolveSystemHealthSource added in v0.1.42

func ResolveSystemHealthSource(ctx context.Context, db *sql.Conn) (SystemHealthSource, error)

ResolveSystemHealthSource locates the running system_health session's event files and its dispatch latency.

type TargetSnapshot added in v0.1.42

type TargetSnapshot struct {
	Events []Event
	// ExcludedKeys are the Event.Keys of events this read DID see but chose not
	// to deliver — driver chatter dropped by ParseRingBuffer, and events removed
	// by the caller's Filter. They exist purely so Drain can tell "we skipped
	// this" apart from "the buffer evicted this before we got to it".
	//
	// Without them the drop metric subtracts a post-filter delivered count from
	// the server's pre-filter totalEventsProcessed, so every deliberately
	// skipped event is reported as lost. That was not hypothetical: on a real
	// intake run it reported ~560 events lost per run, and disabling the noise
	// drop took the same run to zero. The warning was measuring its own filter.
	ExcludedKeys []string
	Stats        TargetStats
}

TargetSnapshot is one read of a session's target — the ring buffer's current contents, or the .xel rows an event_file target has flushed since the previous poll — plus the target's own bookkeeping at that moment.

func ParseRingBuffer

func ParseRingBuffer(payload string) (TargetSnapshot, error)

ParseRingBuffer decodes a ring_buffer target_data payload into the events it holds plus the target's own bookkeeping. Exported so it can be unit-tested against captured fixtures without a DB.

type TargetStats added in v0.1.42

type TargetStats struct {
	Truncated            bool  `json:"truncated,omitempty"`
	ProcessingTime       int64 `json:"processing_time,omitempty"`
	TotalEventsProcessed int64 `json:"total_events_processed,omitempty"`
	EventCount           int64 `json:"event_count"`
	DroppedCount         int64 `json:"dropped_count,omitempty"`
	MemoryUsed           int64 `json:"memory_used,omitempty"`
}

TargetStats is a poll's bookkeeping about the target itself rather than any event in it, and it is the only way to tell that the server produced more events than we read back:

  • TotalEventsProcessed counts every event the session has ever dispatched to the target, so the growth between two polls minus the events we actually delivered is the number the ring buffer evicted unseen;
  • DroppedCount is the server's own count of events it refused to buffer;
  • Truncated reports that the DMV cut target_data short, which silently shortens (or corrupts) the document we parse.

Without these a skipped or failed poll loses events with no trace at all.

A ring_buffer target fills all of them from the <RingBufferTarget> root. An event_file target has no such counters: only DroppedCount is available (from sys.dm_xe_sessions), and TotalEventsProcessed stays zero, which switches off Drain's eviction-delta arm — correctly, because a file target does not evict.

type TraceResult added in v0.1.41

type TraceResult struct {
	SessionName string           `json:"session_name"`
	Database    string           `json:"database"`
	StartedAt   time.Time        `json:"started_at"`
	StoppedAt   time.Time        `json:"stopped_at"`
	Duration    time.Duration    `json:"duration"`
	Events      []Event          `json:"events,omitempty"`
	Ref         *query.EventsRef `json:"ref,omitempty"`
	Preview     []Event          `json:"-"`
	Replays     []ReplayResult   `json:"replays,omitempty"`
	Error       string           `json:"error,omitempty"`

	// Summary is the IO/CPU/timing aggregate over Events, or over Ref when a
	// profiler persisted the events into the paged trace-result store.
	Summary *Summary `json:"summary,omitempty"`
}

TraceResult is the final value returned by the `sql trace` command. It is intentionally serializable as JSON (for --format json) and renders as a single-line summary in text mode. The per-event table is streamed to stdout during collection by the command handler, not rendered from here.

func (TraceResult) Pretty added in v0.1.41

func (r TraceResult) Pretty() api.Text

Pretty renders a one-line summary followed by a table of the captured events. The JSON/YAML formats bypass Pretty and serialize the exported fields instead.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL