tracepanel

package module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 47 Imported by: 0

README

Tracepanel for Go

Tracepanel shows what each request of your app did: its SQL with the line of your code that ran each statement, its logs, its outgoing calls and its panics. It finds what to fix: N+1 queries, duplicates, slow SQL, long transactions. A badge on your pages opens the panel, and the console lists every request.

This SDK records a Go app's requests and sends them to Tracepanel's agent, which keeps them, finds the issues and serves the console. It has no dependencies: the integrations with gin, Echo, pgx, GORM, go-redis, zap, zerolog, gRPC and Kafka are modules of their own, and you get only those you use.

Setup

go get github.com/tracepanel/tracepanel-go

Give sql.Open the driver's name through tracepanel.Driver, and wrap the app's handler:

import tracepanel "github.com/tracepanel/tracepanel-go"

db, err := sql.Open(tracepanel.Driver("pgx"), dsn)
// ...
http.ListenAndServe(addr, tracepanel.WrapHandler(mux))

Run the app with go run .. The first request starts the agent for the project, and the response tells you where its console is: the badge on an HTML page opens it, and Tracepanel-Url names it.

That is all a net/http app with database/sql needs. The calls it makes through http.DefaultClient with the request's context, and what it logs through slog, are recorded too. Other routers, pgx, GORM, Redis, zap, zerolog, gRPC and Kafka take a line each: see What it records.

Development only

Tracepanel records in development only, as every Tracepanel SDK does: when the app runs under go run, with TRACEPANEL_ENABLED=1, or with Config.Enabled. A binary built with go build (your production build, a container's) records nothing: WrapHandler, Driver and the rest hand back what they were given, and no agent is downloaded or started. TRACEPANEL_ENABLED=0 turns it off whatever else says. To record a built binary, run it through the SDK's command, which sets TRACEPANEL_ENABLED=1 for it:

go install github.com/tracepanel/tracepanel-go/cmd/tracepanel@v0.10.0
tracepanel run ./shop

tracepanel run exits with the app's exit code, and says on stderr how an app that failed ended (tracepanel: ./shop exited with status 3). go run github.com/tracepanel/tracepanel-go/cmd/tracepanel@v0.10.0 run ./shop works without installing it, but go run exits 1 whatever the program's code, so that line is where the app's status is.

The agent

The agent is a program of its own, which keeps the traces and finds the issues. You don't install it: the SDK downloads its own version of it the first time, from the agent's npm package for your platform, checks the binary against the SHA-512 the SDK was released with, and keeps it in the user's cache for every project, where it is checked again the first time a process runs it: TRACEPANEL_CACHE_DIR, else %LOCALAPPDATA%\tracepanel\ on Windows, else $XDG_CACHE_HOME/tracepanel/, else ~/.cache/tracepanel/ (macOS too), else the OS temp directory's tracepanel/ when there is no home directory. Behind a proxy, set HTTPS_PROXY; with an npm mirror, TRACEPANEL_NPM_REGISTRY (or npm's own npm_config_registry). TRACEPANEL_AGENT_BIN names another agent, and without a network the SDK runs tracepanel-agent from the PATH.

The SDK and the agent share a minor version: Tracepanel for Go 0.10.x runs tracepanel-agent 0.10.x.

Go 1.26 or later, on Linux, macOS and Windows (go run's binaries are told apart there too, and the agent is the .exe of its win32 package, kept in %LOCALAPPDATA%\tracepanel\).

What it records

  • Each request, named by its route (GET /books/{id}, the ServeMux pattern), with the handler that served it as a view span.
  • SQL through database/sql, whatever runs it (your own queries, sqlx, sqlc, GORM or Bun on the *sql.DB): each statement with its parameters, the rows it returned and the line of your code that ran it, and each transaction around its statements.
  • slog records logged with the request's context (slog.InfoContext(ctx, ...)), with the line that logged them.
  • Calls to HTTP APIs made with the request's context (http.NewRequestWithContext(ctx, ...)) through http.DefaultClient or a client with tracepanel.WrapTransport(base), each with a traceparent, so the app it reaches links its trace to the call. http.Get(url) carries no context, so it isn't recorded. When the panel's Profile asked for the request, its calls carry X-Tracepanel-Profile, so the apps down the chain profile their part.
  • Panics, with your frames and the source around them. The request answers 500.
  • The caller's trace, when a request carries a W3C traceparent.
  • What the request carried, for the console's Request tab: its headers, query, cookies, the body as far as your handler read it (up to 16 KB; JSON and forms with their secrets masked, text as it is, binary left out) and the response's headers. Values under a name that looks secret (Authorization, password, sessionid, api_key) are masked before they are recorded, and none of it leaves your machine. A name is read word by word (camelCase, snake_case, kebab-case, dots), as every Tracepanel SDK reads it: authToken, AUTH_HEADER and X-Api-Key are masked, Author, tokenizer and token_type aren't, and tokens holding numbers (a model's counts) is shown. The SDK never reads a body your handler didn't.
  • Where a request's CPU time went, when the panel's Profile asks for it (see Profiles).

Every response carries Tracepanel-Id and Server-Timing, and an HTML page gets the badge: a page is held until the handler returns, unless it flushes it. Any other response (JSON, a file, a stream) goes out as it is written, and one the handler takes over (a WebSocket) is its own.

The console's App tab shows Go, the router, your module and the modules it depends on, and the build's settings that hold no secret (never -ldflags).

A statement, a log record or a call joins the request's trace through its context: pass r.Context() down, as in db.QueryContext(ctx, ...). Work without it isn't recorded.

Other routers

ServeMux names each route. gin and Echo have middleware of their own (see Integrations). Another router takes tracepanel.Middleware, and Config.Route returns the route, as here for chi:

tracepanel.Setup(tracepanel.Config{
	Route: func(r *http.Request) string {
		return chi.RouteContext(r.Context()).RoutePattern()
	},
})
router.Use(tracepanel.Middleware)
Profiles

The panel's Profile toggle (or a call carrying X-Tracepanel-Profile: 1) records the next requests with a profile: a flame graph of where their CPU time went, from Go's own profiler (runtime/pprof). The request's goroutine carries a profiler label, and so do the goroutines it starts, so a profile holds only that request's samples, however many requests run at once. A task it starts with tracepanel.Go is profiled too. TRACEPANEL_PROFILE=1 (or Config.Profile) profiles every trace, and your code can profile the rest of the trace it runs in:

ctx = tracepanel.Profile(ctx)

A profiled trace says why in profile.reason: header (a request's X-Tracepanel-Profile), cookie (the panel's toggle), parent (a profiled caller asked: a traced call or message, or a task it started), setting or code. When TRACEPANEL_PROFILE=1 would profile the trace anyway, the reason is setting, whatever asked too. Otherwise it is the first ask that profiled the trace, and a later one (tracepanel.Profile in a trace its request's header profiled) doesn't change it.

Go's profiler samples 100 times a second of CPU time: a request that takes less than 10 ms of CPU may have no sample, and time spent waiting (for the database, a lock) isn't in the profile, which the trace's spans show. An app that runs a CPU profile of its own (/debug/pprof/profile) gets no Tracepanel profiles meanwhile.

Go's profiler hands its samples over only once it is stopped, and on macOS stopping it takes up to 100 ms, when nothing is sampled. So it runs on from one profiled request to the next, and is cut once none has run for 200 ms (or, under steady load, every 2 s): back-to-back requests are sampled without a gap, and a profile reaches the console a moment after its request. A request that runs while a cut stops the profiler says for how long (tracepanel.profile_gap_ms).

Every profile has the garbage collector's runs while the request ran, and how long they stopped the program. The panel's Profile → Memory (X-Tracepanel-Profile: memory, or TRACEPANEL_PROFILE_MEMORY=1 for every profiled trace) adds what the heap did meanwhile: the bytes it allocated, its highest level above the start and its level at the end. Go's heap is the whole process's, and nothing the runtime tells of it is per goroutine (its heap profile's samples carry no labels), so these are the heap's numbers while the request ran: the request's own when it ran alone, and marked shared when another one ran meanwhile. Garbage counts until the collector frees it, and there are no allocation sites.

Databases

tracepanel.Driver works wherever a driver's name goes: sqlx.Open, or GORM's DriverName (or use the GORM plugin, not both). For a driver's connector, tracepanel.WrapConnector wraps it for sql.OpenDB:

db := sql.OpenDB(tracepanel.WrapConnector(stdlib.GetConnector(*config)))

The console runs EXPLAIN of a recorded SELECT on the database it came from. With a second database, name it:

replicaDriver := tracepanel.Driver("pgx", tracepanel.DBAlias("replica"))
replica, err := sql.Open(replicaDriver, replicaDSN)

A transaction's statements go under it. For the rest of its work (an HTTP call, a log record, a span), the transaction's context puts it there too, so the console sees a call made inside a transaction:

tx, err := db.BeginTx(ctx, nil)
ctx = tracepanel.TxContext(ctx, tx)

Integrations

Each is a module of its own, so the core keeps no dependencies:

Library Module What it records
gin github.com/tracepanel/tracepanel-go/gintrace requests, named by route
Echo v4 github.com/tracepanel/tracepanel-go/echotrace requests, named by route
Echo v5 github.com/tracepanel/tracepanel-go/echo5trace requests, named by route
pgx v5 github.com/tracepanel/tracepanel-go/pgxtrace SQL and transactions
GORM v2 github.com/tracepanel/tracepanel-go/gormtrace SQL and transactions
go-redis v9 github.com/tracepanel/tracepanel-go/redistrace Redis commands
zap github.com/tracepanel/tracepanel-go/zaptrace log records
zerolog github.com/tracepanel/tracepanel-go/zerologtrace log records
gRPC github.com/tracepanel/tracepanel-go/grpctrace calls served and made
kafka-go github.com/tracepanel/tracepanel-go/kafkatrace messages sent and handled
go get github.com/tracepanel/tracepanel-go/gintrace

Fiber runs on fasthttp, not net/http, and isn't supported yet.

gin
router := gin.Default()
router.Use(gintrace.Middleware())

Each request is named by its route (GET /books/:id), with its handler as a view span. Pass c.Request.Context() down (or c itself, with the engine's ContextWithFallback on).

Echo
e := echo.New()
e.Use(echotrace.Middleware())  // echo5trace.Middleware() for Echo v5

Each request is named by its route (GET /books/:id); on Echo v4 its handler is a view span. An error a handler returns is answered by Echo's error handler within the trace. Pass c.Request().Context() down.

pgx

The tracer records each statement as tracepanel.Driver does, on a pgx.Conn or a pgxpool.Pool:

config, err := pgxpool.ParseConfig(dsn)
config.ConnConfig.Tracer = pgxtrace.Tracer()
pool, err := pgxpool.NewWithConfig(ctx, config)

A transaction's statements go under it. For the rest of its work, pgxtrace.TxContext(ctx, tx) puts it there too. A batch's queries are recorded as they are read, and a CopyFrom as one statement. pgxtrace.Tracer takes the options tracepanel.Driver takes (tracepanel.DBAlias for a second database), and the console runs EXPLAIN on the database a query came from.

GORM
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
err = db.Use(gormtrace.Plugin())

The plugin records each statement as tracepanel.Driver does, with the line of your code that ran it, and each transaction, GORM's own around a create, update or delete too. Run the queries with the request's context, db.WithContext(ctx). For the rest of a transaction's work, gormtrace.TxContext(ctx, tx) puts it under the transaction. The database's kind comes from the dialector (postgres, mysql, sqlite, sqlserver, clickhouse); gormtrace.Plugin takes the options tracepanel.Driver takes, and the console runs EXPLAIN on the database a query came from.

The rows a query returned are there for Find, First, Pluck, Count and for Scan (db.Raw(...).Scan(&rows)), whose count GORM tells its logger: the plugin wraps the gorm.Config.Logger to read it (a Session given a logger of its own after db.Use goes without). Not for Rows() and Row(): your code reads those rows from the *sql.Rows that database/sql hands over, which nothing can watch. With tracepanel.Driver as the dialector's DriverName (in place of the plugin), the driver counts them as it hands each row over.

Use the plugin or tracepanel.Driver as the dialector's DriverName, not both. A database opened through tracepanel.Driver is recorded already, and the plugin leaves it to it.

go-redis
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
redistrace.Instrument(rdb)

Each command is a span with its key, the database and the server, so the agent finds a command repeated for each item (N+1) and duplicates. A pipeline is one span. Cluster and ring clients work too, without the server.

zap and zerolog

zap's loggers carry no context: zaptrace.Context(ctx) is a field that names it, on a record or on a logger, and writes nothing.

logger = logger.WithOptions(zap.WrapCore(zaptrace.WrapCore))
logger.Info("order placed", zaptrace.Context(ctx), zap.Int("order", id))

zerolog takes a writer and a hook, both:

logger := zerolog.New(zerologtrace.Writer(os.Stdout)).Hook(zerologtrace.Hook{})
logger.Info().Ctx(ctx).Int("order", id).Msg("order placed")

A record keeps its level, its message, its fields (secret-looking ones masked, as slog's are) and the line that logged it, when the logger adds the caller.

gRPC
server := grpc.NewServer(grpctrace.ServerOptions()...)
options := append(grpctrace.DialOptions(), grpc.WithTransportCredentials(creds))
conn, err := grpc.NewClient(target, options...)

Each call served is a trace of its own, named Service/Method; a server's fault (Internal, Unavailable, a panic) fails it. Each call made is a span of the trace in its context, and carries a traceparent in its metadata, so the server links its trace to it. A call asks the server to profile its trace with the x-tracepanel-profile metadata key (1, as the HTTP header asks), and a call made in a profiled trace asks the same, so the servers down the chain profile their part. The interceptors are there one by one too (UnaryServerInterceptor, StreamClientInterceptor...), for a chain: a recovery interceptor goes before them.

Kafka

segmentio/kafka-go's writer, wrapped, records what it sends; a consumer hands each message to kafkatrace.Process:

writer := kafkatrace.WrapWriter(&kafka.Writer{Addr: kafka.TCP(broker), Topic: "orders"})
err := writer.WriteMessages(ctx, kafka.Message{Value: order})

msg, err := reader.FetchMessage(ctx)
err = kafkatrace.Process(ctx, msg, handleOrder)

The messages a call sends to a topic are a span (publish orders), and each carries a traceparent. Each message handled is a task trace of its own, named after its handler and linked to the trace that sent it, even in another app with Tracepanel. A message sent in a profiled trace carries tracepanel-profile: 1, as the other SDKs' messages do, and the trace that handles it is profiled too.

Your own code, tasks and commands

// A span of the request's trace, until end().
ctx, end := tracepanel.Span(ctx, "render invoice")
defer end()

// A task: a trace of its own on a goroutine, linked to the request that started it.
tracepanel.Go(ctx, "send welcome email", func(ctx context.Context) error {
	return mailer.Send(ctx, user)
})

// The signed-in user, by id and username, from your authentication.
tracepanel.SetUser(ctx, strconv.FormatInt(user.ID, 10), user.Email)

// A command or a scheduled job: a trace of its own.
err := tracepanel.Command(ctx, "nightly report", report)

A program without an HTTP server (a worker, a command line tool) calls tracepanel.Setup() first, and tracepanel.Flush(time.Second) before it exits, so its traces reach the agent.

Dumps

tracepanel.Dump is a debug print: each value with the expression that gave it, as Go syntax, its type and its length, on stderr. In a request, the dump is in its trace too, in the call tree where it ran.

tracepanel.Dump(ctx, order, user.Roles)
handlers.go:42 checkout
    order: &main.Order{
        ID:    7,
        Items: []main.Item{},
    } (*main.Order)
    user.Roles: []string{"admin"} ([]string) len=1

It prints in every build, recorded or not. A value whose expression names a secret (password, token, apiKey) is masked, and so is a value inside one under such a name: a struct's field (Password, APIKey) or a map's key ("session_id"), printed and recorded alike. A long value is cut at 16 KB.

Settings

tracepanel.WrapHandler(mux, tracepanel.Config{...}) and tracepanel.Setup(...) take a Config; with Enabled, call Setup before tracepanel.Driver, so the database is recorded too. What it leaves empty comes from the environment:

Field Variable Default
Enabled TRACEPANEL_ENABLED on under go run, off in a built binary; 1 records in any build, 0 never
Service TRACEPANEL_SERVICE the executable's name
StoreDir TRACEPANEL_DIR .tracepanel, the project's store
ConsoleURL TRACEPANEL_CONSOLE_URL where the project's agent serves it
MaxTraces TRACEPANEL_MAX_TRACES 2000, the traces the agent keeps
Profile TRACEPANEL_PROFILE off; 1 profiles every trace, not only those the panel asks for
ProfileMemory TRACEPANEL_PROFILE_MEMORY off; 1 measures every profiled trace's memory, not only those asked for it
TRACEPANEL_PROFILE_ENABLED on; 0 profiles nothing
TRACEPANEL_CAPTURE_REQUEST on; 0 keeps no headers, query, cookies or body
TRACEPANEL_AGENT_BIN the one the SDK downloads
TRACEPANEL_AGENT auto; an agent elsewhere (tcp:host:port), never started; off starts none
TRACEPANEL_CACHE_DIR %LOCALAPPDATA%\tracepanel on Windows, else $XDG_CACHE_HOME/tracepanel or ~/.cache/tracepanel, else the OS temp directory's tracepanel when there is no home directory, where the downloaded agent is kept
TRACEPANEL_NPM_REGISTRY npm's registry, where the agent is downloaded from
TRACEPANEL_N_PLUS_ONE_THRESHOLD 3, repeats that make an N+1
TRACEPANEL_N_PLUS_ONE_MIN_MS 5, the time repeated cache calls must cost together
TRACEPANEL_SLOW_SQL_MS 100, a slow query
TRACEPANEL_LONG_TRANSACTION_MS 500, a long transaction
TRACEPANEL_SLOW_HTTP_MS 500, a slow outgoing call
TRACEPANEL_QUEUE_WAIT_MS 1000, a task that waited too long for a worker
TRACEPANEL_LARGE_RESULT_ROWS 1000, a query without a LIMIT that read too many rows

TRACEPANEL_SLOW_SQL_MS and TRACEPANEL_LONG_TRANSACTION_MS take a limit per database too, by its kind: TRACEPANEL_SLOW_SQL_MS=100,snowflake=3000 holds every database to 100 ms but Snowflake, to 3 s. Without one, the agent holds an analytical warehouse (Snowflake, BigQuery, Databricks, Redshift, ClickHouse) to 2 s per query. The kind comes from the driver's name: pgx and lib/pq are postgresql, gosnowflake's is snowflake. The drivers of BigQuery, Databricks, Redshift, Trino, ClickHouse, DuckDB, MySQL, MariaDB, SQLite, SQL Server and Oracle are told the same way. For another database, tracepanel.DBSystem("cockroachdb") names it.

Without an agent (none could be had, or it couldn't start), the log says why once. Responses keep their headers, pages get no badge, and traces aren't kept.

MCP

For an AI coding agent (Claude Code, Cursor, Codex), the same traces over MCP, from the project's directory, with the agent the SDK runs, through the tracepanel command (go install github.com/tracepanel/tracepanel-go/cmd/tracepanel@v0.10.0):

claude mcp add --scope project tracepanel -- tracepanel mcp

Without installing it, go run github.com/tracepanel/tracepanel-go/cmd/tracepanel@v0.10.0 mcp does the same. The command runs the agent's console in the foreground (tracepanel dev --port 8765, and tracepanel dev -- ./shop with the app beside it, recording), a built app recording (tracepanel run ./shop) and the agent with its own arguments (tracepanel agent --help).

Development

make lint       # gofmt, go vet, the tests of every module, every module built for Windows
make cross      # every module vetted and built for Windows, amd64 and arm64
make e2e        # an app against the real agent (e2e/, a module of its own)
make integrity  # the checksums of AgentVersion's binaries, once it is on npm

make integrity (or go generate) writes internal/agentbin/integrity.go, the SHA-512 of each platform's agent binary, after AgentVersion changes in version.go; a release checks the tag is Version, AgentVersion of its minor, and the checksums of AgentVersion. The tests that run the agent take a build of the sibling tracepanel checkout, or the one TRACEPANEL_AGENT_BIN names; make e2e downloads it otherwise, as an app does.

Each integration is a module of its own, with a replace of the core to the checkout, which Go ignores in a dependency. A release tags the core (v0.10.0) and then, on the same commit, each integration at the same version (gintrace/v0.10.0): each must require the core of the tag, and is built without its replace against the core the module proxy has.

License

MIT

Documentation

Overview

Package tracepanel is Tracepanel for Go: it records each request of an app (its SQL with the line of code that ran each statement, its logs, its outgoing HTTP calls, its panics) and sends it to the Tracepanel agent, which finds the issues (N+1 queries, duplicates, slow SQL), keeps the traces in .tracepanel/ and serves the console. The middleware adds what the badge on a page reads (Server-Timing, Tracepanel-Id) and puts the badge into HTML pages.

db, err := sql.Open(tracepanel.Driver("pgx"), dsn)
http.ListenAndServe(addr, tracepanel.WrapHandler(mux))

It has no dependencies, and records nothing until WrapHandler or Setup turns it on, in development only: under `go run`, with TRACEPANEL_ENABLED=1 or Config.Enabled; a binary `go build` wrote records nothing, and TRACEPANEL_ENABLED=0 always wins. The agent comes with it: the first trace starts the one the SDK downloads (once, into the user's cache). Its MCP server, for AI coding agents, runs with the tracepanel command:

go install github.com/tracepanel/tracepanel-go/cmd/tracepanel@v0.10.0
tracepanel mcp

Index

Examples

Constants

View Source
const AgentVersion = "0.10.0"

AgentVersion is the agent this SDK runs, and downloads when it isn't installed: of the same minor as Version, a patch of its own. After changing it, `go generate` (or `make integrity`) writes the checksums of its binaries, which a download must match; a release checks they are of AgentVersion.

View Source
const Version = "0.10.0"

Version is this SDK's version.

Variables

This section is empty.

Functions

func Command

func Command(ctx context.Context, name string, fn func(context.Context) error) error

Command records fn as a trace of its own: a scheduled job, a CLI command, a migration. An error it returns, or a panic, fails the trace (the panic goes on).

Example

A command or a scheduled job, a trace of its own; Flush before the program exits.

package main

import (
	"context"
	"log"
	"log/slog"
	"time"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	tracepanel.Setup()
	defer tracepanel.Flush(5 * time.Second)
	err := tracepanel.Command(context.Background(), "nightly cleanup", func(ctx context.Context) error {
		slog.InfoContext(ctx, "old sessions removed", "count", 42)
		return nil
	})
	if err != nil {
		log.Print(err)
	}
}

func Driver

func Driver(name string, options ...DBOption) string

Driver is the name of the driver `name` with its statements recorded, for sql.Open (and sqlx.Open, GORM's DriverName, anything that opens by a driver's name; for GORM, this or gormtrace.Plugin, not both):

db, err := sql.Open(tracepanel.Driver("pgx"), dsn)

The first call registers it with database/sql, as "tracepanel:pgx". When Tracepanel is off (outside development), it is `name`.

Example

The database opened through the wrapped driver: each statement run with a request's context is recorded, with the line that ran it.

package main

import (
	"database/sql"
	"log"
	"net/http"
	"os"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	db, err := sql.Open(tracepanel.Driver("pgx"), os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	http.HandleFunc("GET /authors", func(w http.ResponseWriter, r *http.Request) {
		// With the request's context: QueryContext, not Query.
		rows, err := db.QueryContext(r.Context(), "SELECT id, name FROM authors")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer rows.Close()
	})
}
Example (Options)

A second database, a replica, named apart, of a kind its driver's name doesn't tell.

package main

import (
	"database/sql"
	"log"
	"os"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	replica, err := sql.Open(
		tracepanel.Driver("pgx", tracepanel.DBAlias("replica"), tracepanel.DBSystem("postgresql")),
		os.Getenv("REPLICA_URL"),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer replica.Close()
}

func Dump

func Dump(ctx context.Context, values ...any)

Dump prints values to stderr as a debug print does, each with the expression that gave it, its pretty Go-syntax text, its type and its length, under the line that called it:

tracepanel.Dump(ctx, order, user.Roles)

handlers.go:42 checkout
    order: &main.Order{
        ID:    7,
        Items: []main.Item{},
    } (*main.Order)
    user.Roles: []string{"admin"} ([]string) len=1

It always prints, also when Tracepanel isn't recording. When ctx carries a trace, the dump is recorded too, a log record on its current span that the console shows in the call tree. A value whose expression names a secret (password, token, api_key) is masked, printed and recorded, and so is a value inside one under such a name: a struct's field (Password, APIKey), a map's key ("token").

Example

A debug print: the values with their expressions on stderr, and in the request's trace.

package main

import (
	"net/http"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	http.HandleFunc("GET /cart", func(w http.ResponseWriter, r *http.Request) {
		cart := map[string]int{"tea": 2}
		tracepanel.Dump(r.Context(), cart)
	})
}

func Flush

func Flush(timeout time.Duration)

Flush waits until the traces recorded so far went to the agent, or `timeout` passed: a command calls it before it exits.

func Go

func Go(ctx context.Context, name string, fn func(context.Context) error)

Go runs fn on a goroutine of its own as a task: a trace of its own, linked to the trace in ctx that started it, where the start is a span. Its context keeps ctx's values but isn't canceled with it, so a task outlives the request that started it.

Example

Work that outlives the request, as a task: a trace of its own, linked to the request that started it.

package main

import (
	"context"
	"log/slog"
	"net/http"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	http.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
		tracepanel.Go(r.Context(), "send receipt", func(ctx context.Context) error {
			slog.InfoContext(ctx, "receipt sent")
			return nil
		})
		w.WriteHeader(http.StatusAccepted)
	})
}

func Middleware

func Middleware(next http.Handler) http.Handler

Middleware is WrapHandler as a router's middleware, for chi's r.Use, gorilla's or echo's WrapMiddleware:

r.Use(tracepanel.Middleware)

Setup, called before it, gives it a Config. When Tracepanel is off, it returns the handler as it is.

func Profile

func Profile(ctx context.Context) context.Context

Profile profiles the trace in ctx from here on, as the panel's Profile toggle profiles a request from its start: where its CPU time goes from now until it ends, a flame graph in the console (profile.reason "code"). The current goroutine is profiled; a goroutine the code starts for the trace is too when started with the context it returns.

ctx = tracepanel.Profile(ctx)

A trace profiled already (asked for, or TRACEPANEL_PROFILE=1) stays as it is; outside a trace, or with profiling off (TRACEPANEL_PROFILE_ENABLED=0), it returns ctx.

func SetUser

func SetUser(ctx context.Context, id, username string)

SetUser names the signed-in user on the trace in ctx, as the app's authentication knows them: their id (user.id) and their username (user.username, left out when empty). The username stays on this machine: the agent drops it from what leaves it.

tracepanel.SetUser(r.Context(), strconv.FormatInt(user.ID, 10), user.Email)
Example

The signed-in user, named on the request's trace.

package main

import (
	"net/http"
	"strconv"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	type user struct {
		ID    int64
		Email string
	}
	http.HandleFunc("GET /account", func(w http.ResponseWriter, r *http.Request) {
		signedIn := user{ID: 7, Email: "ada@example.com"}
		tracepanel.SetUser(r.Context(), strconv.FormatInt(signedIn.ID, 10), signedIn.Email)
	})
}

func Setup

func Setup(config ...Config)

Setup turns Tracepanel on for the process, once: the standard slog logger's records and the default HTTP client's calls join the traces. WrapHandler and Middleware call it. A program without an HTTP server (a worker, a command) calls it itself, and Flush before it exits. It turns Tracepanel on in development only: under `go run`, with TRACEPANEL_ENABLED=1 or with Config.Enabled; TRACEPANEL_ENABLED=0 leaves it off whatever else says.

func Span

func Span(ctx context.Context, name string) (context.Context, func())

Span records a piece of the app's own code as a span of the trace in ctx, until the function it returns is called. Work done with the context it returns goes under it:

ctx, end := tracepanel.Span(ctx, "render invoice")
defer end()
Example

A piece of the app's own code as a span.

package main

import (
	"log/slog"
	"net/http"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	http.HandleFunc("GET /invoice", func(w http.ResponseWriter, r *http.Request) {
		ctx, end := tracepanel.Span(r.Context(), "render invoice")
		defer end()
		slog.InfoContext(ctx, "rendered")
	})
}

func TxContext

func TxContext(ctx context.Context, tx *sql.Tx) context.Context

TxContext is ctx for the work of the transaction `tx`, begun with ctx: what runs with it while `tx` is open (an outgoing HTTP call, a log record, a span, a query) goes under the transaction's span, and after the commit or the rollback under ctx's span again. database/sql keeps the context a transaction began with to itself, so the app asks for this one:

tx, err := db.BeginTx(ctx, nil)
ctx = tracepanel.TxContext(ctx, tx)

Outside a trace, or for a transaction that isn't recorded, it is ctx.

func WrapConnector

func WrapConnector(connector driver.Connector, options ...DBOption) driver.Connector

WrapConnector is a driver's connector with its statements recorded, for sql.OpenDB:

db := sql.OpenDB(tracepanel.WrapConnector(stdlib.GetConnector(*config)))

When Tracepanel is off (outside development), it is `connector`.

func WrapHandler

func WrapHandler(handler http.Handler, config ...Config) http.Handler

WrapHandler is Tracepanel in one line, around the app's handler (its ServeMux):

http.ListenAndServe(addr, tracepanel.WrapHandler(mux))

It turns Tracepanel on (Setup), and traces every request: its handler on a ServeMux is a `view` span named after its function, and the response gets what the badge reads, its headers (Tracepanel-Id, Server-Timing) and, in an HTML page, the badge itself. A page is held until the handler returns; any other response goes out as it is written. When Tracepanel is off (outside development), it returns the handler as it is.

Example

The app's ServeMux, wrapped: each request a trace, its handler a span, the badge on its pages.

package main

import (
	"fmt"
	"log"
	"net/http"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /books/{id}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "book %s", r.PathValue("id"))
	})
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", tracepanel.WrapHandler(mux)))
}

func WrapLogHandler

func WrapLogHandler(next slog.Handler) slog.Handler

WrapLogHandler adds slog records to the trace of the context they were logged with (slog.InfoContext(ctx, ...)), then passes them on to `next`. Setup puts it in front of the standard logger's handler; a logger of the app's own takes it as its handler:

logger := slog.New(tracepanel.WrapLogHandler(slog.NewJSONHandler(os.Stdout, nil)))

A record keeps its level, its message, the line that logged it, and each attribute as an extra field (extra.name, a group's extra.group.name), a secret-looking one masked.

func WrapTransport

func WrapTransport(base http.RoundTripper) http.RoundTripper

WrapTransport records the calls made through `base` (http.DefaultTransport when nil):

client := &http.Client{Transport: tracepanel.WrapTransport(nil)}

Setup gives it to http.DefaultClient, so its calls are recorded already. http.DefaultTransport stays as it is: code asserts it is an *http.Transport. A call is recorded when its request has the context of a traced request: use http.NewRequestWithContext(ctx, ...). It carries the trace's traceparent, and X-Tracepanel-Profile when the request was asked to be profiled, so the app it reaches links its trace to the call.

Example

An HTTP client of the app's own, whose calls are recorded and carry the trace's traceparent.

package main

import (
	"net/http"
	"time"

	tracepanel "github.com/tracepanel/tracepanel-go"
)

func main() {
	client := &http.Client{Transport: tracepanel.WrapTransport(nil), Timeout: 10 * time.Second}
	http.HandleFunc("GET /rates", func(w http.ResponseWriter, r *http.Request) {
		request, _ := http.NewRequestWithContext(r.Context(), "GET", "https://api.example.com/rates", nil)
		response, err := client.Do(request)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadGateway)
			return
		}
		defer response.Body.Close()
	})
}

Types

type Config

type Config struct {
	// The app's name in the console: TRACEPANEL_SERVICE, else the executable's name.
	Service string
	// Where the agent serves the console: the badge loads from there and opens it. TRACEPANEL_CONSOLE_URL, else the
	// address the project's agent writes (console.addr), on whatever port it got.
	ConsoleURL string
	// The project's store directory, where the agent keeps the traces: TRACEPANEL_DIR, else .tracepanel.
	StoreDir string
	// The app's module path: frames under it are the project's code. The main module's, read from the build.
	Module string
	// The most traces the agent keeps for the project: TRACEPANEL_MAX_TRACES, else 2000.
	MaxTraces int
	// The route of a request, for a router that doesn't set Request.Pattern as ServeMux does: "/books/{id}".
	Route func(*http.Request) string
	// Record in this build, not only under `go run` or with TRACEPANEL_ENABLED=1: a staging build, say. Set up
	// before Driver, so the database is recorded too. TRACEPANEL_ENABLED=0 still turns Tracepanel off.
	Enabled bool
	// Profile every trace (where its CPU time went, a flame graph in the console), not only a request that asks
	// with the panel's Profile toggle: TRACEPANEL_PROFILE=1. TRACEPANEL_PROFILE_ENABLED=0 profiles nothing.
	Profile bool
	// Measure every profiled trace's memory (what the heap allocated while it ran), not only a request's that asks
	// for "memory": TRACEPANEL_PROFILE_MEMORY=1.
	ProfileMemory bool
}

Config is optional: what it leaves empty comes from TRACEPANEL_* variables, then defaults.

type DBOption

type DBOption func(*database)

DBOption tells Driver or WrapConnector more about the database.

func DBAlias

func DBAlias(name string) DBOption

DBAlias names the database among the app's ("default"): a second one, such as a replica, gets a name of its own.

func DBName

func DBName(name string) DBOption

DBName is the database's name, shown with its queries (the alias otherwise).

func DBSystem

func DBSystem(name string) DBOption

DBSystem is the database's kind, as OpenTelemetry names it ("postgresql", "mysql", "sqlite"), when the driver's name doesn't tell.

Directories

Path Synopsis
cmd
tracepanel command
Command tracepanel runs the Tracepanel agent the Go SDK runs (the one in the user's cache, downloaded the first time), for what a Go project has no pip or npm to run it with, and a built app recording:
Command tracepanel runs the Tracepanel agent the Go SDK runs (the one in the user's cache, downloaded the first time), for what a Go project has no pip or npm to run it with, and a built app recording:
echo5trace module
echotrace module
gintrace module
gormtrace module
grpctrace module
internal
agentbin
Package agentbin finds the Tracepanel agent's binary for a Go app, which has no pip or npm to install it: the agent this SDK was released with, downloaded once from its npm platform package (the same binary as on PyPI and Maven Central), checked against the SHA-512 this SDK holds for it (integrity.go), and kept in the user's cache (~/.cache/tracepanel/agent/VERSION/PLATFORM/), shared by every project and every SDK.
Package agentbin finds the Tracepanel agent's binary for a Go app, which has no pip or npm to install it: the agent this SDK was released with, downloaded once from its npm platform package (the same binary as on PyPI and Maven Central), checked against the SHA-512 this SDK holds for it (integrity.go), and kept in the user's cache (~/.cache/tracepanel/agent/VERSION/PLATFORM/), shared by every project and every SDK.
hook
Package hook is what the integrations (pgxtrace, gintrace, grpctrace and the others, each a module of its own) record traces with: the core package fills these in when it loads, so the rules of a trace stay in one place.
Package hook is what the integrations (pgxtrace, gintrace, grpctrace and the others, each a module of its own) record traces with: the core package fills these in when it loads, so the rules of a trace stay in one place.
integrity command
Command integrity writes internal/agentbin/integrity.go: the SHA-512 of the agent's binary for each platform, at the version AgentVersion names in version.go (or -version), which the SDK checks a downloaded agent against.
Command integrity writes internal/agentbin/integrity.go: the SHA-512 of the agent's binary for each platform, at the version AgentVersion names in version.go (or -version), which the SDK checks a downloaded agent against.
tracetest
Package tracetest records the traces of an integration's tests, as the agent would get them.
Package tracetest records the traces of an integration's tests, as the agent would get them.
kafkatrace module
pgxtrace module
redistrace module
zaptrace module
zerologtrace module

Jump to

Keyboard shortcuts

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