trace

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 2026 License: MIT Imports: 13 Imported by: 0

README

trace — transparent SQL tracing for SQLite

trace wraps the modernc.org/sqlite driver at the database/sql/driver level. Switching from "sqlite" to "sqlite-trace" records every Exec and Query without changing application code.

sql.Open("sqlite-trace", "app.db")
        │
        ▼
  TracingDriver.Open
        │
        ▼
  tracingConn.Prepare ─► tracingStmt
        │                    │
        ▼                    ▼
  driver.Conn          Exec / Query
                             │
                    ┌────────┴────────┐
                    ▼                 ▼
              slog (adaptive)   Store (async SQLite)
              Debug < 100ms     batch 64 / flush 1s
              Warn  ≥ 100ms    channel 1024 (drop-on-full)
              Error on failure

Quick start

import _ "github.com/hazyhaar/pkg/trace"

// 1. Set up the trace store (uses raw "sqlite" to avoid recursion).
traceDB, _ := sql.Open("sqlite", "traces.db")
store := trace.NewStore(traceDB)
store.Init()
trace.SetStore(store)
defer store.Close()

// 2. Open your app database with the tracing driver.
db, _ := sql.Open("sqlite-trace", "app.db")
// All queries are now traced automatically.

How it works

  1. Driver wrappingTracingDriver wraps every Conn and Stmt returned by the base SQLite driver. The init() function registers it as "sqlite-trace".

  2. Adaptive logging — Each query is logged via slog at a level that depends on duration: Debug under 100 ms, Warn at 100 ms+, Error on failure. The trace ID from kit.GetTraceID(ctx) is included when present.

  3. Async persistence — If a Store is configured, entries are sent to a 1024-capacity channel. A background goroutine batches up to 64 entries or flushes every second, inserting them in a single transaction.

Schema

CREATE TABLE sql_traces (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    trace_id   TEXT,
    op         TEXT NOT NULL,       -- "Exec" or "Query"
    query      TEXT NOT NULL,
    duration_us INTEGER NOT NULL,
    error      TEXT,
    timestamp  INTEGER NOT NULL     -- unix microseconds
);

Indexes on timestamp, trace_id (partial, non-empty), and duration_us (partial, > 100 000 us) for slow-query analysis.

Remote tracing (FO → BO)

In a FO/BO split architecture, the FO has no local SQLite for traces. RemoteStore batches entries and POSTs them over HTTPS to the BO, which ingests them into its local Store. This follows the same HTTPS pattern as authproxy and dbsync.WriteProxy.

  FO                                    BO
  ┌──────────────┐   HTTPS POST   ┌──────────────┐
  │ RemoteStore   │──────────────▶│ IngestHandler │
  │ (batch 64,    │  JSON []*Entry │ (RecordAsync  │
  │  flush 1s)    │               │  into Store)  │
  └──────────────┘               └──────────────┘

FO side:

rs := trace.NewRemoteStore("https://bo.internal/api/internal/traces", nil)
trace.SetStore(rs)
defer rs.Close()

BO side:

mux.Handle("/api/internal/traces", trace.IngestHandler(store))

Exported API

Symbol Description
Recorder Interface for trace backends (RecordAsync + Close)
Store Async trace writer with batching (local SQLite)
NewStore(db) Create store (db must use raw "sqlite" driver)
RemoteStore Async trace forwarder (HTTPS POST to BO)
NewRemoteStore(url, client) Create remote store (nil client = 5s default)
IngestHandler(store) HTTP handler for BO trace ingestion endpoint
Entry Single trace record
SetStore(s) Set / replace global recorder (nil disables persistence)
Schema DDL string for manual migration

Documentation

Overview

Package trace provides transparent SQL tracing for modernc.org/sqlite.

It registers a "sqlite-trace" driver that wraps the standard "sqlite" driver, intercepting every Exec and Query at the database/sql/driver level. No application code changes are needed beyond switching the driver name:

import _ "github.com/hazyhaar/pkg/trace"  // registers "sqlite-trace"

// Trace store (opened with raw "sqlite" to avoid recursion)
traceDB, _ := sql.Open("sqlite", "traces.db")
store := trace.NewStore(traceDB)
store.Init()
trace.SetStore(store)

// Application DB — all queries are now traced automatically
db, _ := sql.Open("sqlite-trace", "app.db")

Without a Store (SetStore not called or nil), the driver still logs every query via slog with adaptive levels (Debug, Warn >100ms, Error on failure). Trace IDs are read from context via kit.GetTraceID for request correlation.

Index

Constants

View Source
const Schema = `` /* 477-byte string literal not displayed */

Schema for the sql_traces table. Call Store.Init() or apply manually.

Variables

This section is empty.

Functions

func IngestHandler

func IngestHandler(store *Store) http.HandlerFunc

IngestHandler returns an HTTP handler that receives trace batches from a RemoteStore (FO side) and writes them to the local Store (BO side).

Expected request: POST with application/json body containing []*Entry. Returns 204 on success, 405 for wrong method, 400 for bad payload.

Mount on the BO:

mux.Handle("/api/internal/traces", trace.IngestHandler(store))

func SetStore

func SetStore(s Recorder)

SetStore sets the global trace recorder for persistence. Accepts a Store (local SQLite) or RemoteStore (HTTP to BO). Pass nil to disable persistence (slog-only mode).

Types

type Entry

type Entry struct {
	TraceID    string // correlation with HTTP/MCP request
	Op         string // "Exec" or "Query"
	Query      string // SQL statement
	DurationUs int64  // microseconds
	Error      string // empty if success
	Timestamp  int64  // unix microseconds
}

Entry is a single SQL trace record.

type Recorder

type Recorder interface {
	RecordAsync(e *Entry)
	Close() error
}

Recorder is the interface for trace persistence backends. Store (local SQLite) and RemoteStore (HTTP POST to BO) both implement it.

type RemoteStore

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

RemoteStore sends trace entries to a BO endpoint via HTTP POST. It uses the same async batching pattern as Store: a 1024-capacity channel, batches of up to 64, flushed every second.

Usage (FO side):

rs := trace.NewRemoteStore("https://bo.example.com/api/internal/traces", nil)
trace.SetStore(rs)
defer rs.Close()

func NewRemoteStore

func NewRemoteStore(url string, client *http.Client) *RemoteStore

NewRemoteStore creates a RemoteStore that POSTs trace batches to url. If client is nil, a default client with 5s timeout is used.

func (*RemoteStore) Close

func (rs *RemoteStore) Close() error

Close drains the buffer and stops the flush goroutine.

func (*RemoteStore) RecordAsync

func (rs *RemoteStore) RecordAsync(e *Entry)

RecordAsync queues an entry for async push. Non-blocking; drops if buffer full.

type Store

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

Store persists SQL trace entries to a SQLite table asynchronously. It MUST be opened with the raw "sqlite" driver (not "sqlite-trace") to avoid infinite recursion.

func NewStore

func NewStore(db *sql.DB) *Store

NewStore creates a trace store backed by the given database connection. The db should use the raw "sqlite" driver to avoid tracing its own writes.

func (*Store) Close

func (s *Store) Close() error

Close drains the buffer and stops the flush goroutine.

func (*Store) Init

func (s *Store) Init() error

Init creates the sql_traces table if it doesn't exist.

func (*Store) RecordAsync

func (s *Store) RecordAsync(e *Entry)

RecordAsync queues an entry for async persistence. Non-blocking; drops if buffer full.

type TracingDriver

type TracingDriver struct {
	driver.Driver
}

TracingDriver wraps the modernc.org/sqlite driver, intercepting every Exec and Query at the database/sql/driver level.

Registered as "sqlite-trace" in init(). Open connections with sql.Open("sqlite-trace", path) to get automatic tracing.

func (*TracingDriver) Open

func (d *TracingDriver) Open(name string) (driver.Conn, error)

Jump to

Keyboard shortcuts

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