devtools

package
v0.1.31 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// LevelHeader names the capture level, using observability's own level
	// vocabulary rather than a parallel scale.
	LevelHeader = "X-Debug-Level"

	// LevelParam says the same thing for a caller that cannot set a header —
	// a browser following a download link, which builds its own URL.
	LevelParam = "__debug"

	// IDHeader correlates a response with the record it produced.
	IDHeader = "X-Debug-Id"

	// RefreshHeader asks this request to rebuild every metadata lookup it makes
	// instead of reading the cache.
	//
	// A header rather than a console-wide setting, because it is a property of
	// the run you are about to make: leaving it on would quietly turn every
	// subsequent page into a cache miss, which is exactly the cost the cache
	// exists to avoid.
	RefreshHeader = "X-Debug-Refresh-Inspection"
)
View Source
const (
	// DefaultMaxRecords is how many execution summaries the history keeps. It is
	// a session's worth of queries, not a day's.
	DefaultMaxRecords = 200

	// DefaultMaxLogLines bounds the process-wide tail.
	DefaultMaxLogLines = 2000

	// DefaultMaxDetailBytes bounds the bodies and previews held across all
	// records. Detail is evicted oldest-first when this is exceeded; the summary
	// it belonged to survives, so the history stays complete and only the
	// expensive half thins out.
	DefaultMaxDetailBytes = 32 << 20

	// DefaultDetailTTL is how long detail is worth keeping. It matches the
	// session cap: past it, nobody is still debugging that request.
	DefaultDetailTTL = 15 * time.Minute
)

Variables

This section is empty.

Functions

func Arm

func Arm(options ArmOptions) (dbcontext.Context, *query.Recorder, error)

Arm returns the context this request's executions must run on, plus the recorder collecting them.

A request that asked for nothing gets its context back unchanged and a nil recorder, which every recorder method tolerates — so the unarmed path, which is nearly every request, costs one header read.

func NewRequestRecorder

func NewRequestRecorder(options ArmOptions) (*query.Recorder, error)

NewRequestRecorder mints the recorder a request asked for, without needing a database context — which the arming middleware does not have, and does not need, because the handler that runs the executions lifts the recorder onto its own context later.

func RecorderFromRequest

func RecorderFromRequest(r *http.Request) *query.Recorder

RecorderFromRequest returns the recorder this request was armed with, or nil.

Handlers need this because the execution context is derived from the server's context rather than the request's — only cancellation is bridged — so a recorder cannot ride down on request-context values alone. Lifting it is one explicit line at each entry point, which is better than a hidden channel that works on three paths and silently does not on a fourth.

func RefreshRequested

func RefreshRequested(r *http.Request) bool

RefreshRequested reports whether a request asked to rebuild what it inspects.

func RequestWithRecorder

func RequestWithRecorder(r *http.Request, recorder *query.Recorder) *http.Request

RequestWithRecorder is how a request becomes armed. The middleware calls it, and so does any test that needs to exercise a handler's armed path without standing up the whole chain in front of it.

func RequestedLevel

func RequestedLevel(r *http.Request) (level logger.LogLevel, armed bool, err error)

RequestedLevel reads the capture level a request asked for.

An unknown name is an error rather than a default: a client that sent "verbose" and silently got "info" would report a bug against the wrong layer.

func StampID

StampID writes the correlation header and makes sure a browser can read it.

The expose list has to be appended at write time rather than set here: the handlers downstream call setCORSHeaders while producing the response, which replaces whatever this middleware had already put there.

func TeeProcessLogs

func TeeProcessLogs(store *Store) func()

TeeProcessLogs sends every line the process logs to store as well as to wherever it was already going, and returns a function restoring the previous writer.

logger.SetOutput is global and atomic and affects every named logger, existing and future, which is what makes this reach background work. The original writer stays first in the chain, so the operator's terminal is byte-for-byte unchanged.

func WithRequestRecorder

func WithRequestRecorder(ctx dbcontext.Context, r *http.Request) dbcontext.Context

WithRequestRecorder is the lift itself: the context an entry point is about to execute on, carrying the recorder its request was armed with.

An unarmed request gets its context back untouched rather than one carrying a nil recorder, so nothing downstream can tell an armed-at-nothing run from an ordinary one — there is no such state.

Types

type ArmOptions

type ArmOptions struct {
	Context dbcontext.Context
	Request *http.Request
	Source  query.ExecutionSource
	// NewID mints the record's correlation handle. Injected so a test can assert
	// on a stable id instead of matching a pattern.
	NewID func() string
}

ArmOptions configures Arm.

type Capabilities

type Capabilities struct {
	Enabled  bool     `json:"enabled"`
	Levels   []string `json:"levels"`
	Header   string   `json:"header"`
	Param    string   `json:"param"`
	IDHeader string   `json:"idHeader"`
	Stats    Stats    `json:"stats"`
}

Capabilities tells a console what this server will do before it asks for it — which levels it accepts and what it will retain — so the UI is not built around constants duplicated on the other side of the wire.

type ErrDetailEvicted

type ErrDetailEvicted struct {
	ID     string
	Reason string
}

ErrDetailEvicted distinguishes "that detail aged out" from "no such record". Conflating them would tell a user their id was wrong when it was not.

func (*ErrDetailEvicted) Error

func (e *ErrDetailEvicted) Error() string

type Handler

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

func New

func New(options HandlerOptions) *Handler

New builds the devtools handler. Wrap the rest of the chain with Handler so requests it does not own fall through unchanged.

func (*Handler) Handler

func (h *Handler) Handler(next http.Handler) http.Handler

Handler returns h wrapping next.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Handler) Store

func (h *Handler) Store() *Store

Store is the record store this handler serves, so the server can hand the same one to the arming middleware.

type HandlerOptions

type HandlerOptions struct {
	Prefix string
	Store  *Store

	// Enabled gates the whole surface. A server told to hide error details is not
	// one that may hand out queries, headers and response bodies through a side
	// door, so the caller passes that decision in rather than this package
	// guessing at it.
	Enabled bool
}

HandlerOptions configures New.

type InspectionCaches

type InspectionCaches struct {
	Caches []inspection.CacheStats `json:"caches"`
}

InspectionCaches is what the metadata caches currently hold.

It is the other half of the Inspection tab. A record says what one request looked up; this says what the server is holding on everyone's behalf, which is the thing an operator flushes.

type Options

type Options struct {
	MaxRecords     int
	MaxLogLines    int
	MaxDetailBytes int64
	DetailTTL      time.Duration
}

Options configures NewStore.

type RecordsPage

type RecordsPage struct {
	Records []query.ExecutionSummary `json:"records"`
	Stats   Stats                    `json:"stats"`
}

RecordsPage carries the history plus what the store has let go, so a console that finds a gap between its last sequence and the next can tell a quiet server from a full buffer.

type Ring

type Ring[T any] struct {
	// contains filtered or unexported fields
}

func NewRing

func NewRing[T any](options RingOptions[T]) *Ring[T]

func (*Ring[T]) Append

func (r *Ring[T]) Append(item T) int64

Append stores an item and delivers it to every current subscriber, returning the sequence it was given.

It never blocks: a subscriber whose channel is full misses this item and the ring keeps it, so the stream degrades for one reader rather than stalling the query that produced it.

func (*Ring[T]) Clear

func (r *Ring[T]) Clear()

Clear discards everything retained. Sequences keep climbing: a client holding a Last-Event-ID from before the clear must not be handed sequences it has already seen.

func (*Ring[T]) Dropped

func (r *Ring[T]) Dropped() int64

Dropped is how many items the ring has evicted. A client that finds a gap between its Last-Event-ID and the next sequence needs this to tell "nothing happened" from "I stopped reading for too long".

func (*Ring[T]) Items

func (r *Ring[T]) Items() []T

Items returns everything the ring holds, oldest first.

func (*Ring[T]) ItemsAfter

func (r *Ring[T]) ItemsAfter(after int64) []T

ItemsAfter returns everything past a sequence the caller already holds.

func (*Ring[T]) OldestSequence

func (r *Ring[T]) OldestSequence() int64

OldestSequence is the lowest sequence still retained, or 0 when empty.

func (*Ring[T]) SubscribeFrom

func (r *Ring[T]) SubscribeFrom(after int64) (replay []T, live <-chan T, cancel func())

SubscribeFrom returns everything past `after` that the ring still holds, plus a channel carrying everything appended from now on.

Both are produced under one lock so there is no window in which an item is in neither: a replay taken before registering would miss anything appended in between, and registering first would deliver it twice.

type RingOptions

type RingOptions[T any] struct {
	// Max is how many items the ring retains. Older items are evicted first.
	Max int

	// Stamp writes the assigned sequence into the item. A consumer that reads an
	// item on its own — out of a replay, out of an SSE frame — still has to know
	// where it sits in the stream, and threading that back through every call
	// site is how items end up unstamped in one path and stamped in another.
	Stamp func(item *T, sequence int64)
}

RingOptions configures NewRing.

type Stats

type Stats struct {
	Records         int   `json:"records"`
	RecordsDropped  int64 `json:"recordsDropped"`
	OldestSequence  int64 `json:"oldestSequence"`
	LogLines        int   `json:"logLines"`
	LogsDropped     int64 `json:"logsDropped"`
	LogsOldest      int64 `json:"logsOldestSequence"`
	DetailsHeld     int   `json:"detailsHeld"`
	DetailBytes     int64 `json:"detailBytes"`
	DetailsEvicted  int   `json:"detailsEvicted"`
	MaxDetailBytes  int64 `json:"maxDetailBytes"`
	DetailTTLSecond int64 `json:"detailTtlSeconds"`
}

Stats reports what the store holds and what it has let go, so a client that finds a gap in its sequences can tell a quiet server from a full buffer.

type Store

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

func NewStore

func NewStore(options Options) *Store

func (*Store) Add

func (s *Store) Add(recorder *query.Recorder) query.ExecutionSummary

Add files a finished recorder: the summary goes to every open console, the detail is held for whoever opens that row.

func (*Store) Clear

func (s *Store) Clear()

Clear discards the history. Sequences keep climbing so a console resuming from a Last-Event-ID is not replayed ids it has already applied.

func (*Store) Detail

func (s *Store) Detail(id string) (query.ExecutionDetail, error)

Detail returns one record's expensive half. It reports eviction as an error rather than as an empty record, because "we no longer hold it" and "it made no requests" are different answers.

func (*Store) Log

func (s *Store) Log(line query.LogLine)

Log appends one line to the process-wide tail.

func (*Store) Logs

func (s *Store) Logs(after int64) []query.LogLine

Logs returns tail lines past a sequence the caller already holds.

func (*Store) Records

func (s *Store) Records(after int64) []query.ExecutionSummary

Records returns the summaries past a sequence the caller already holds.

func (*Store) Stats

func (s *Store) Stats() Stats

func (*Store) SubscribeLogs

func (s *Store) SubscribeLogs(after int64) ([]query.LogLine, <-chan query.LogLine, func())

func (*Store) SubscribeRecords

func (s *Store) SubscribeRecords(after int64) ([]query.ExecutionSummary, <-chan query.ExecutionSummary, func())

SubscribeRecords and SubscribeLogs are the two streams a console follows.

Jump to

Keyboard shortcuts

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