Documentation
¶
Index ¶
- Constants
- func Arm(options ArmOptions) (dbcontext.Context, *query.Recorder, error)
- func NewRequestRecorder(options ArmOptions) (*query.Recorder, error)
- func RecorderFromRequest(r *http.Request) *query.Recorder
- func RefreshRequested(r *http.Request) bool
- func RequestWithRecorder(r *http.Request, recorder *query.Recorder) *http.Request
- func RequestedLevel(r *http.Request) (level logger.LogLevel, armed bool, err error)
- func StampID(w http.ResponseWriter, id string) http.ResponseWriter
- func TeeProcessLogs(store *Store) func()
- func WithRequestRecorder(ctx dbcontext.Context, r *http.Request) dbcontext.Context
- type ArmOptions
- type Capabilities
- type ErrDetailEvicted
- type Handler
- type HandlerOptions
- type InspectionCaches
- type Options
- type RecordsPage
- type Ring
- func (r *Ring[T]) Append(item T) int64
- func (r *Ring[T]) Clear()
- func (r *Ring[T]) Dropped() int64
- func (r *Ring[T]) Items() []T
- func (r *Ring[T]) ItemsAfter(after int64) []T
- func (r *Ring[T]) OldestSequence() int64
- func (r *Ring[T]) SubscribeFrom(after int64) (replay []T, live <-chan T, cancel func())
- type RingOptions
- type Stats
- type Store
- func (s *Store) Add(recorder *query.Recorder) query.ExecutionSummary
- func (s *Store) Clear()
- func (s *Store) Detail(id string) (query.ExecutionDetail, error)
- func (s *Store) Log(line query.LogLine)
- func (s *Store) Logs(after int64) []query.LogLine
- func (s *Store) Records(after int64) []query.ExecutionSummary
- func (s *Store) Stats() Stats
- func (s *Store) SubscribeLogs(after int64) ([]query.LogLine, <-chan query.LogLine, func())
- func (s *Store) SubscribeRecords(after int64) ([]query.ExecutionSummary, <-chan query.ExecutionSummary, func())
Constants ¶
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" )
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 ¶
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 ¶
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 ¶
RefreshRequested reports whether a request asked to rebuild what it inspects.
func RequestWithRecorder ¶
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 ¶
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 ¶
func StampID(w http.ResponseWriter, id string) http.ResponseWriter
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 ¶
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 ¶
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.
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 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 ¶
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 ¶
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 ¶
ItemsAfter returns everything past a sequence the caller already holds.
func (*Ring[T]) OldestSequence ¶
OldestSequence is the lowest sequence still retained, or 0 when empty.
func (*Ring[T]) SubscribeFrom ¶
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 (*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) Records ¶
func (s *Store) Records(after int64) []query.ExecutionSummary
Records returns the summaries past a sequence the caller already holds.
func (*Store) SubscribeLogs ¶
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.