Documentation
¶
Index ¶
- Constants
- func AdminAuth(adminKey string, next http.Handler) http.Handler
- func FilterHeaders(h http.Header) map[string]string
- func NewTraceID() string
- func ParseUserAgent(ua string) (browser, os string)
- func ShouldPersistTrace(s Sample) bool
- func TraceIDFromCtx(ctx context.Context) string
- func WithSpanTracker(ctx context.Context) context.Context
- func WithTraceID(ctx context.Context, id string) context.Context
- type Alert
- type Alerter
- type AnomalyDetector
- func (d *AnomalyDetector) GetCount(tenantID string) int64
- func (d *AnomalyDetector) IncrCounter(tenantID string)
- func (d *AnomalyDetector) Observe(tenantID string, ms float64) (bool, float64)
- func (d *AnomalyDetector) RecentAnomalies(tenantID string, n int) []AnomalyEvent
- func (d *AnomalyDetector) RecordAnomaly(tenantID string, latencyUS, z float64)
- type AnomalyEvent
- type Check
- type CheckResult
- type CooldownAlerter
- type DynamicCheck
- type ErrGroup
- type ErrorCapture
- type ErrorStore
- type Frame
- type FullSnapshot
- type GeoLookup
- type HistoryPoint
- type Metrics
- func (m *Metrics) Gatherer() prometheus.Gatherer
- func (m *Metrics) Handler() http.Handler
- func (m *Metrics) IncGoroutinePanic()
- func (m *Metrics) IncRequestPanic()
- func (m *Metrics) ObserveMigration(tenantID, status string, durationSeconds float64)
- func (m *Metrics) ObserveRequest(tenantID, method, path, status string, durationSeconds float64)
- func (m *Metrics) SetActiveTenants(n int)
- type MinuteBucket
- type NoopAlerter
- type ObsServer
- type ObsStore
- func (s *ObsStore) Close() error
- func (s *ObsStore) Flush(tenantID string, snap Snapshot) error
- func (s *ObsStore) History(tenantID string, hours int) ([]Snapshot, error)
- func (s *ObsStore) Path() string
- func (s *ObsStore) Prune() error
- func (s *ObsStore) SaveSlowTrace(tenantID string, tv TraceView) error
- func (s *ObsStore) SlowTraces(tenantID string, hours int) ([]TraceView, error)
- type PercentileSnapshot
- type RecentRequest
- type Rings
- func (rs *Rings) Count() int
- func (rs *Rings) Recent(tenantID string) []RecentRequest
- func (rs *Rings) RecentTraces(tenantID string, n int) []TraceView
- func (rs *Rings) Record(tenantID string, s Sample)
- func (rs *Rings) RouteID(route string) uint16
- func (rs *Rings) RouteName(id uint16) string
- func (rs *Rings) Snapshot(tenantID string) []Sample
- func (rs *Rings) TenantIDs() []string
- type SLOConfig
- type SLOEngine
- type SLOSnapshot
- type Sample
- type SlackAlerter
- type Snapshot
- type Span
- type SpanTracker
- type SyntheticMonitor
- type TenantHistogram
- type TenantRing
- type TraceView
Constants ¶
const ( LevelWarning = "warning" LevelCritical = "critical" )
Alert levels.
const PersistErrors = true
PersistErrors, when true, persists every error trace (status >= 400) regardless of latency, so a fast 401/403/422/500 is still captured for debugging.
const SlowTraceThresholdUS = 50_000
SlowTraceThresholdUS is the per-request duration above which a trace is persisted to slow_traces (Phase 1: a fixed 50ms, not a per-tenant p95).
const TraceIDKey = contextKey("trace_id")
TraceIDKey is the context key under which the per-request trace id is stored.
Variables ¶
This section is empty.
Functions ¶
func AdminAuth ¶
AdminAuth wraps next so it is reachable only with a matching X-Admin-Key header. This is the single gate shared by the /debug/* endpoints and /metrics.
func FilterHeaders ¶
FilterHeaders flattens an http.Header into a name→value map, redacting the values of sensitive (credential) headers to "[Filtered]". Only the first value of each header is kept. Called only when a trace is persisted (off the hot path), so its allocation never taxes the 200-OK common case.
func NewTraceID ¶
func NewTraceID() string
NewTraceID returns a 16-hex-character trace id (8 random bytes, no hyphens), using crypto/rand and the stdlib only — no google/uuid. crypto/rand.Read in modern Go is backed by a fast userspace CSPRNG, so this stays cheap.
func ParseUserAgent ¶
ParseUserAgent extracts a coarse browser and OS from a User-Agent string using stdlib-only substring matching — no external dependency, ~1µs. Unknown values fall back to "Unknown".
Note: Android UAs also contain "Linux" (Android is Linux-based) and iOS UAs contain "like Mac OS X", so Android/iOS are matched BEFORE Linux/macOS.
func ShouldPersistTrace ¶
ShouldPersistTrace reports whether a request's trace should be written to slow_traces. A trace is persisted when it is either slow (DurUS above the threshold) or — when PersistErrors — an error response (Status >= 400).
(A third heuristic, "any span with dur_us == 0", was considered for detecting a prematurely-cut pipeline but rejected: sub-microsecond stages such as the RBAC map lookup legitimately round to 0µs, so it would persist almost every request. "How far the pipeline got" is instead conveyed by which spans are present — error paths now mark their own stage.)
func TraceIDFromCtx ¶
TraceIDFromCtx returns the trace id from ctx, or "" if none is set.
func WithSpanTracker ¶
WithSpanTracker returns a context carrying a fresh SpanTracker.
Types ¶
type Alert ¶
type Alert struct {
TenantID string
Level string // LevelWarning | LevelCritical
Message string
BurnRate float64
P95ms float64
}
Alert is a single SLO-breach notification.
type Alerter ¶
Alerter delivers SLO alerts somewhere (Slack, a no-op sink, …).
func NewSlackAlerterFromEnv ¶
func NewSlackAlerterFromEnv() Alerter
NewSlackAlerterFromEnv returns a SlackAlerter when SLACK_WEBHOOK_URL is set, or a NoopAlerter otherwise — so the server always starts cleanly without alerting config.
type AnomalyDetector ¶
type AnomalyDetector struct {
// contains filtered or unexported fields
}
AnomalyDetector flags requests whose latency is statistically anomalous using an exponentially-weighted moving average of mean and variance.
func NewAnomalyDetector ¶
func NewAnomalyDetector() *AnomalyDetector
func (*AnomalyDetector) GetCount ¶
func (d *AnomalyDetector) GetCount(tenantID string) int64
GetCount returns the total anomaly count for tenantID since startup.
func (*AnomalyDetector) IncrCounter ¶
func (d *AnomalyDetector) IncrCounter(tenantID string)
IncrCounter increments the anomaly count for tenantID by one.
func (*AnomalyDetector) Observe ¶
func (d *AnomalyDetector) Observe(tenantID string, ms float64) (bool, float64)
Observe updates the EWMA model for tenantID with a new latency sample (µs). Returns (true, z-score) when the sample is anomalous, (false, 0) otherwise.
func (*AnomalyDetector) RecentAnomalies ¶
func (d *AnomalyDetector) RecentAnomalies(tenantID string, n int) []AnomalyEvent
RecentAnomalies returns up to n of tenantID's most recent anomalies, newest first. Returns a non-nil empty slice when none are recorded, so it marshals to [] not null.
func (*AnomalyDetector) RecordAnomaly ¶
func (d *AnomalyDetector) RecordAnomaly(tenantID string, latencyUS, z float64)
RecordAnomaly increments the tenant's anomaly counter AND appends the event to its recent-anomaly ring. Call it (instead of IncrCounter) when Observe reports an anomaly: latencyUS is the offending request's latency and z the detected z-score. Off the common request path — runs only on a detection (z > zThreshold).
type AnomalyEvent ¶
type AnomalyEvent struct {
TS int64 `json:"ts"` // unix microseconds (matches trace TS)
LatencyUS float64 `json:"latency_us"` // request latency in microseconds
ZScore float64 `json:"z_score"`
}
AnomalyEvent is one detected latency anomaly, retained so the admin observability panel can show WHEN it happened, the request latency, and the z-score — data the detector already computes on detection but previously only logged + counted.
type Check ¶
type Check struct {
Name string
URL string
Method string
Headers map[string]string
Expected int // expected HTTP status code; 0 = any 2xx
}
Check describes one synthetic health probe.
type CheckResult ¶
type CheckResult struct {
Name string `json:"name"`
Status string `json:"status"`
LatencyMs int64 `json:"latency_ms"`
LastCheck time.Time `json:"last_check"`
LastError string `json:"last_error,omitempty"`
Uptime float64 `json:"uptime_pct"`
// contains filtered or unexported fields
}
CheckResult is the last known state of a synthetic check.
type CooldownAlerter ¶
type CooldownAlerter struct {
// contains filtered or unexported fields
}
CooldownAlerter wraps another Alerter and drops repeat alerts for the same (tenant, level) pair that arrive within the cooldown window.
func NewCooldownAlerter ¶
func NewCooldownAlerter(inner Alerter, cooldown time.Duration) *CooldownAlerter
NewCooldownAlerter wraps inner so that each (tenant, level) fires at most once per cooldown.
type DynamicCheck ¶
DynamicCheck resolves its probe lazily on every tick. Resolve returns the Check to run, or nil plus a human-readable reason — reported as status "pending" instead of a failing probe. This exists for canaries whose target only makes sense once external state exists (e.g. an API canary on a fresh install, before any tenant is registered: probing would just spam 4xx for a route nobody created).
type ErrGroup ¶
type ErrGroup struct {
Count atomic.Int64
FirstSeen int64
LastSeen atomic.Int64
Message string
Stack []Frame // symbolized only on first occurrence
// contains filtered or unexported fields
}
ErrGroup aggregates repeated occurrences of the same logical error.
type ErrorCapture ¶
type ErrorCapture struct {
TraceID string `json:"trace_id"`
TenantID string `json:"tenant_id"`
Route string `json:"route"`
Method string `json:"method"`
ErrMsg string `json:"error_msg"`
Stack []Frame `json:"stack"`
UserID string `json:"user_id"`
Role string `json:"role"`
Timestamp int64 `json:"ts"`
}
ErrorCapture is a snapshot of a server error (HTTP 500) linked to the request's trace, so the stack trace can be shown next to the timeline. For client errors (4xx) the Stack is empty — those are not server bugs and never pay for runtime.Callers. UserID/Role/Route/Method are filled by the caller (the handler) because observability must not import auth (auth imports observability).
func CaptureError ¶
func CaptureError(ctx context.Context, err error) ErrorCapture
CaptureError snapshots the current goroutine's stack for a server error (500) and links it to the request's trace_id and tenant (from ctx). It symbolizes the stack at most once per call site; repeat occurrences reuse the cached frames with ZERO allocations.
The hit path captures only 3 PCs into a stack-local array used solely for the fingerprint (so nothing escapes to the heap); the expensive full-stack capture + symbolization lives in captureAndSymbolize, called only on a cache miss, so its unavoidable PC-slice escape never taxes the common (repeat) path.
Returned BY VALUE so a caller that discards it allocates nothing. NEVER call on the happy path — even a hit pays for runtime.Callers.
type ErrorStore ¶
type ErrorStore struct {
// contains filtered or unexported fields
}
ErrorStore deduplicates errors per tenant by fingerprinting type + message prefix + call site.
func NewErrorStore ¶
func NewErrorStore() *ErrorStore
func (*ErrorStore) Record ¶
func (es *ErrorStore) Record(tenantID string, err error)
Record fingerprints err, captures a cheap PC snapshot on every call, and symbolizes the stack exactly once per unique fingerprint via stackOnce.
type Frame ¶
type Frame struct {
Function string `json:"function"`
File string `json:"file"`
Line int `json:"line"`
}
Frame is a single symbolized stack frame.
type FullSnapshot ¶
type FullSnapshot struct {
Cached *PercentileSnapshot `json:"cached,omitempty"`
Uncached *PercentileSnapshot `json:"uncached,omitempty"`
}
FullSnapshot splits latency into cache-hit vs cache-miss buckets.
type GeoLookup ¶
type GeoLookup struct {
// contains filtered or unexported fields
}
GeoLookup resolves an IP to an ISO country code using an in-memory GeoLite2 database. Lookups are ~1µs and never touch the network. A GeoLookup whose database failed to load returns "" for everything — graceful degradation, so a missing/corrupt mmdb never blocks startup or panics.
func DefaultGeoLookup ¶
func DefaultGeoLookup() *GeoLookup
DefaultGeoLookup builds a lookup from the embedded GeoLite2 database.
func NewGeoLookup ¶
NewGeoLookup builds a lookup from a GeoLite2-Country mmdb buffer. An empty or invalid buffer yields a usable no-op GeoLookup (Country → "") rather than an error.
type HistoryPoint ¶
type HistoryPoint struct {
TS int64 `json:"ts"`
P50US int64 `json:"p50_us"`
P95US int64 `json:"p95_us"`
BurnRate float64 `json:"burn_rate"`
ErrorRatio float64 `json:"error_ratio"`
SLOStatus string `json:"slo_status"`
}
HistoryPoint is the trimmed per-snapshot projection returned under "history". BurnRate/ErrorRatio come straight from the persisted snapshot so the panel can plot the SLO burn-rate over time (with the multi-window thresholds overlaid) without a second query.
type Metrics ¶
type Metrics struct {
// contains filtered or unexported fields
}
Metrics holds the four Prometheus collectors described in DEPLOYMENT_INFRA.md, backed by a dedicated registry rather than the global default. The dedicated registry keeps construction side-effect-free (safe to build more than once, e.g. in tests) and scopes /metrics output to exactly these series plus the standard Go/process collectors.
func (*Metrics) Gatherer ¶
func (m *Metrics) Gatherer() prometheus.Gatherer
Gatherer exposes the dedicated registry as a prometheus.Gatherer. It enables end-to-end metric assertions (prometheus/testutil.GatherAndCompare) against exactly this Metrics' series, without reaching for the global default registry. Like Handler(), it carries no auth of its own.
func (*Metrics) Handler ¶
Handler returns the Prometheus exposition handler scoped to this registry. It carries no auth of its own — mount it behind the admin-key middleware.
func (*Metrics) IncGoroutinePanic ¶
func (m *Metrics) IncGoroutinePanic()
IncGoroutinePanic records one panic recovered inside a Ctx.SafeGo goroutine.
func (*Metrics) IncRequestPanic ¶
func (m *Metrics) IncRequestPanic()
IncRequestPanic records one panic recovered by the request-chain Recoverer.
func (*Metrics) ObserveMigration ¶
ObserveMigration records the duration and outcome of a tenant migration.
func (*Metrics) ObserveRequest ¶
ObserveRequest records one served request: increments the counter and observes the duration histogram. path should be the chi route pattern (e.g. "/api/{entity}") rather than the raw URL path, to keep label cardinality bounded.
func (*Metrics) SetActiveTenants ¶
SetActiveTenants updates the gauge of tenants currently loaded in cache.
type MinuteBucket ¶
MinuteBucket is one minute of aggregated traffic. Three uint32 fields, stored in a fixed array — no per-minute allocations.
type NoopAlerter ¶
type NoopAlerter struct{}
NoopAlerter discards alerts (used when no SLACK_WEBHOOK_URL is configured).
type ObsServer ¶
type ObsServer struct {
// contains filtered or unexported fields
}
ObsServer exposes internal observability data over HTTP.
func NewObsServer ¶
func NewObsServer( hist *TenantHistogram, errors *ErrorStore, anomaly *AnomalyDetector, synthmon *SyntheticMonitor, rings *Rings, slo *SLOEngine, store *ObsStore, ) *ObsServer
func (*ObsServer) DebugRouter ¶
DebugRouter returns an admin-gated sub-router serving /tenant/{id} and /synthetic. Mount it at "/debug" on any router (the main :8080 server or the control plane).
func (*ObsServer) Router ¶
Router returns a chi.Mux exposing the /debug/... paths, admin-protected. Mount at "/" on an existing router (used by the control plane on :9090).
func (*ObsServer) ServeTenantData ¶
func (s *ObsServer) ServeTenantData(w http.ResponseWriter, r *http.Request)
ServeTenantData serves the per-tenant observability JSON (the same payload as the admin-gated GET /debug/tenant/{id}), reading the tenant id from the chi "id" URL param. It performs NO authorization itself — the caller (e.g. the admin API in pkg/platformadmin) is responsible for authorizing the request first (platform super-admin → any tenant; tenant admin → its own). This exists so the observability logic is REUSED with a different authorization gate, never duplicated. The data is already tenant-scoped (filtered by tenant_id), so no cross-tenant data can leak through it (R5 of the persistence audit).
func (*ObsServer) SetGeo ¶
SetGeo installs the GeoLite2 lookup used to enrich recent_traces with country.
func (*ObsServer) SetTracesHandler ¶
SetTracesHandler installs the handler served at /debug/traces. It is injected from package main (where the HTML is go:embed'd and the admin key lives) so the route applies ITS OWN auth — query param (?key=) OR X-Admin-Key header — which is intentionally looser than the header-only gate on the JSON debug APIs, so the page can be opened directly in a browser.
type ObsStore ¶
type ObsStore struct {
// contains filtered or unexported fields
}
ObsStore persists observability snapshots to a local SQLite file (modernc, no CGO).
func OpenStore ¶
OpenStore opens (creating if needed) the SQLite store at path and ensures the schema exists. An empty path selects defaultObsDBPath (a persistent location). The parent directory is created if missing; if it cannot be created or written, the store falls back to an ephemeral temp file so observability still works — a bad path is logged as a WARNING, never a boot failure. A resolved path on an ephemeral filesystem (/tmp or a tmpfs) is also logged, so the operator knows the history will not survive a restart.
func (*ObsStore) History ¶
History returns the tenant's snapshots from the last `hours`, newest first.
func (*ObsStore) Path ¶
Path returns the resolved on-disk location the store opened. It may differ from the requested path when an unwritable directory forced the ephemeral fallback.
func (*ObsStore) Prune ¶
Prune enforces retention: it deletes snapshots and slow traces older than the retention window, AND caps slow_traces at maxSlowTraceRows (oldest dropped). It must be called periodically, not only at startup — otherwise a long-running server never reclaims space.
func (*ObsStore) SaveSlowTrace ¶
SaveSlowTrace persists a single slow request's trace (id, route, total, span breakdown). tv.TS is the request start in unix microseconds, consistent with the ring's Sample.Start. Designed to be called asynchronously from the request path so it never blocks the response.
type PercentileSnapshot ¶
type PercentileSnapshot struct {
P50Us float64 `json:"p50_us"`
P95Us float64 `json:"p95_us"`
P99Us float64 `json:"p99_us"`
P999Us float64 `json:"p999_us"`
Count int64 `json:"count"`
Mean float64 `json:"mean_us"`
}
PercentileSnapshot holds a point-in-time latency summary for one bucket. TenantID is intentionally absent — the response envelope provides it.
type RecentRequest ¶
type RecentRequest struct {
StartUS int64 `json:"start_us"`
DurUS int32 `json:"dur_us"`
QueryUS int32 `json:"query_us"`
Route string `json:"route"`
Status uint16 `json:"status"`
}
RecentRequest is the JSON-friendly projection of a Sample with its route id resolved back to a human-readable pattern.
type Rings ¶
type Rings struct {
// contains filtered or unexported fields
}
Rings holds one TenantRing per tenant plus a route-pattern interner that maps variable-length route strings to compact uint16 ids stored in each Sample.
func (*Rings) Count ¶
Count returns the number of distinct tenants that have recorded at least one request.
func (*Rings) Recent ¶
func (rs *Rings) Recent(tenantID string) []RecentRequest
Recent returns the tenant's recent requests as RecentRequest values, ordered most-recent first, with route ids resolved to strings.
func (*Rings) RecentTraces ¶
RecentTraces returns up to n of the tenant's most recent requests projected as TraceViews (trace id resolved to hex, route resolved to string, spans copied out of the fixed array).
func (*Rings) Record ¶
Record appends a sample to the given tenant's ring, creating the ring on first use.
func (*Rings) RouteID ¶
RouteID returns a stable uint16 id for route, interning it on first sight. Reads are lock-free-ish (RLock); only the first occurrence of a route takes the write lock. Ids saturate at the uint16 max to avoid overflow wraparound.
type SLOConfig ¶
type SLOConfig struct {
TargetUptime float64 // e.g. 0.999 (99.9%)
LatencySLOms float64 // e.g. 100.0 ms — requests slower than this count as errors
AlertCooldown time.Duration // e.g. 15min between repeat alerts of the same level
}
SLOConfig holds the per-tenant objectives and alert cadence.
func DefaultSLOConfig ¶
func DefaultSLOConfig() SLOConfig
DefaultSLOConfig is the objective applied to every tenant unless overridden.
type SLOEngine ¶
type SLOEngine struct {
// contains filtered or unexported fields
}
SLOEngine rolls the per-tenant request ring up into minute buckets every 60s, evaluates multi-window burn rates, and fires alerts through alerter. It is fully independent of the AnomalyDetector.
func NewSLOEngine ¶
func NewSLOEngine(rings *Rings, hist *TenantHistogram, alerter Alerter) *SLOEngine
NewSLOEngine builds an engine with the default SLO config. The provided alerter is wrapped in a CooldownAlerter so each (tenant, level) fires at most once per cooldown.
func NewSLOEngineWithConfig ¶
func NewSLOEngineWithConfig(rings *Rings, hist *TenantHistogram, alerter Alerter, cfg SLOConfig) *SLOEngine
NewSLOEngineWithConfig is NewSLOEngine with an explicit config (used in tests to shrink the cooldown window).
func (*SLOEngine) Run ¶
Run evaluates all tenants immediately, then every 60s until ctx is cancelled.
func (*SLOEngine) Snapshot ¶
func (e *SLOEngine) Snapshot(tid string) SLOSnapshot
Snapshot returns the current SLO state for a tenant (used by /debug and tests).
type SLOSnapshot ¶
type SLOSnapshot struct {
ErrorRatio5m float64 `json:"error_ratio_5m"`
BurnRate5m float64 `json:"burn_rate_5m"`
ErrorRatio1h float64 `json:"error_ratio_1h"`
BurnRate1h float64 `json:"burn_rate_1h"`
Status string `json:"status"`
}
SLOSnapshot is the JSON projection exposed under /debug/tenant/{id} → "slo".
type Sample ¶
type Sample struct {
Start int64
DurUS int32
QueryUS int32
Route uint16
Status uint16
TraceID [8]byte
Spans [maxSpans]Span
NSpans uint8
// ErrMsg is the error message for an errored request ("" otherwise).
// ErrorCapture carries the symbolized stack for a 500 (nil otherwise) — a
// pointer, so non-error samples cost only 8 bytes and Record stays alloc-free.
ErrMsg string
ErrorCapture *ErrorCapture
// Client context (raw): browser/OS are parsed and country geo-resolved lazily
// in the projection, so the hot path only stores two strings.
IP string
UserAgent string
}
Sample is a fixed-size record of a single request. All fields are value types so a Sample never escapes to the heap and TenantRing.Record stays allocation-free.
Start request start time, unix microseconds DurUS total request duration, microseconds QueryUS time spent in DB queries, microseconds (0 when unmeasured) Route interned route-pattern id (resolve via Rings.RouteName) Status HTTP status code TraceID first 8 bytes of the request trace id (16 hex chars decoded) Spans per-stage durations captured by the request's SpanTracker NSpans number of valid entries in Spans
Spans/TraceID are value types too, so Record stays allocation-free.
type SlackAlerter ¶
type SlackAlerter struct {
// contains filtered or unexported fields
}
SlackAlerter posts alerts to a Slack incoming-webhook URL. The HTTP client is the shared SSRF-safe egress client (blocks loopback/private/ link-local and refuses redirects), so a misconfigured webhook can't be used to reach internal services.
func NewSlackAlerter ¶
func NewSlackAlerter(webhookURL string) *SlackAlerter
NewSlackAlerter builds a SlackAlerter posting to webhookURL with a 5s timeout.
type Snapshot ¶
type Snapshot struct {
TenantID string `json:"tenant_id"`
TS int64 `json:"ts"` // unix seconds
P50US int64 `json:"p50_us"`
P95US int64 `json:"p95_us"`
ErrorRatio float64 `json:"error_ratio"`
BurnRate float64 `json:"burn_rate"`
SLOStatus string `json:"slo_status"`
}
Snapshot is one persisted point-in-time observation for a tenant.
type Span ¶
Span is one timed stage within a request. Name is a constant literal (e.g. "jwt"), so storing it never allocates.
type SpanTracker ¶
type SpanTracker struct {
// HasError/ErrMsg are set on ANY error response (4xx/5xx) via RecordError.
// Capture is set only for 500s (carries the symbolized stack) via SetCapture.
HasError bool
ErrMsg string
Capture *ErrorCapture
// contains filtered or unexported fields
}
SpanTracker records up to maxSpans stage durations for a single request. It is a fixed array (not a slice) so Mark never allocates and the tracker never escapes beyond the one heap allocation made by NewSpanTracker. It is NOT safe for concurrent Mark calls — but a request is handled by one goroutine through the middleware chain, so the spans are marked sequentially.
func NewSpanTracker ¶
func NewSpanTracker() *SpanTracker
NewSpanTracker starts a tracker whose clock begins now.
func SpanTrackerFromCtx ¶
func SpanTrackerFromCtx(ctx context.Context) *SpanTracker
SpanTrackerFromCtx returns the SpanTracker in ctx, or nil if none is set. Callers must nil-check before calling Mark.
func (*SpanTracker) ElapsedUS ¶ added in v0.1.11
func (t *SpanTracker) ElapsedUS() int64
ElapsedUS is the engine time spent on the request so far: the marked stages plus the tail since the last mark. TotalUS counts only the marks.
func (*SpanTracker) Finish ¶
func (t *SpanTracker) Finish() []Span
Finish records a final "done" span (the tail since the last mark) and returns the recorded spans. The returned slice is backed by the tracker's array — no allocation — and must be copied if it needs to outlive the tracker.
func (*SpanTracker) Mark ¶
func (t *SpanTracker) Mark(name string)
Mark records the elapsed time since the previous Mark (or since creation) as a span named name. Once maxSpans are recorded, further marks are ignored (the clock still advances), so a runaway caller can never overflow or panic.
func (*SpanTracker) RecordError ¶
func (t *SpanTracker) RecordError(msg string)
RecordError marks the request as errored and stores the error message. Cheap (no stack): used for client errors (401/403/422) and as the message for 500s.
func (*SpanTracker) SetCapture ¶
func (t *SpanTracker) SetCapture(c *ErrorCapture)
SetCapture attaches a symbolized error capture (the stack) to the request — used only for server errors (500). Implies HasError.
func (*SpanTracker) Spans ¶ added in v0.1.11
func (t *SpanTracker) Spans() []Span
TotalUS returns the sum of all recorded span durations. Spans returns the stages marked so far (a copy) — what a handler can publish to the client BEFORE the response is written (Server-Timing).
func (*SpanTracker) TotalUS ¶
func (t *SpanTracker) TotalUS() int32
type SyntheticMonitor ¶
type SyntheticMonitor struct {
// contains filtered or unexported fields
}
SyntheticMonitor periodically probes a list of HTTP endpoints.
func NewSyntheticMonitor ¶
func NewSyntheticMonitor(checks []Check) *SyntheticMonitor
func (*SyntheticMonitor) AddDynamic ¶
func (sm *SyntheticMonitor) AddDynamic(d DynamicCheck)
AddDynamic registers a lazily-resolved check. Must be called before Start.
func (*SyntheticMonitor) Results ¶
func (sm *SyntheticMonitor) Results() map[string]*CheckResult
Results returns all current check results as a map keyed by check name.
type TenantHistogram ¶
type TenantHistogram struct {
// contains filtered or unexported fields
}
TenantHistogram records request latency per tenant using HDR histograms, split by whether the response was served from cache.
func NewTenantHistogram ¶
func NewTenantHistogram() *TenantHistogram
func (*TenantHistogram) FullSnapshot ¶
func (th *TenantHistogram) FullSnapshot(tenantID string) *FullSnapshot
FullSnapshot returns the cached/uncached latency split for a tenant, or nil buckets when no data has been recorded for that category.
func (*TenantHistogram) Record ¶
func (th *TenantHistogram) Record(tenantID string, durationUs int64, fromCache bool)
Record adds a latency sample (in microseconds) for a given tenant. fromCache distinguishes cache hits (sub-ms) from full DB-backed responses.
func (*TenantHistogram) Snapshot ¶
func (th *TenantHistogram) Snapshot(tenantID string) *PercentileSnapshot
Snapshot returns the uncached latency snapshot. Kept for test backward-compatibility; prefer FullSnapshot in production paths.
type TenantRing ¶
type TenantRing struct {
// contains filtered or unexported fields
}
TenantRing is a fixed circular buffer of the last ringSize samples for one tenant.
A mutex — not lock-free atomics — guards the buffer on purpose: once head laps the array, two writers separated by ringSize records target the SAME physical slot, so an atomic head only serializes index allocation, not the 24-byte Sample copy. Those concurrent same-slot writes are a genuine data race (caught by -race). The lock keeps Record allocation-free (a single array assignment, no slices).
func (*TenantRing) Record ¶
func (r *TenantRing) Record(s Sample)
Record stores s in the next slot, overwriting the oldest sample once the buffer is full. Allocation-free.
func (*TenantRing) Snapshot ¶
func (r *TenantRing) Snapshot() []Sample
Snapshot returns a copy of the retained samples ordered most-recent first. Returns a non-nil empty slice (not nil) when nothing has been recorded, so it marshals to a JSON [] rather than null.
type TraceView ¶
type TraceView struct {
TraceID string `json:"trace_id"`
TS int64 `json:"ts"` // request start, unix microseconds
Route string `json:"route"`
TotalUS int32 `json:"total_us"`
Status uint16 `json:"status"`
Spans []Span `json:"spans"`
ErrMsg string `json:"error_msg,omitempty"` // set for error responses
Stack []Frame `json:"stack,omitempty"` // set only for 500s
// Request context for the error panel — populated for recent_traces from the
// in-memory ErrorCapture (slow_traces persists only error_msg + stack).
Method string `json:"method,omitempty"`
UserID string `json:"user_id,omitempty"`
Role string `json:"role,omitempty"`
// Client context — IP, parsed browser/OS, and geo country.
IP string `json:"ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Browser string `json:"browser,omitempty"`
OS string `json:"os,omitempty"`
Country string `json:"country,omitempty"`
// Reproducible-request context (persisted traces only): filtered headers +
// full URL. Method (above) completes the curl reconstruction.
Headers map[string]string `json:"headers,omitempty"`
FullURL string `json:"full_url,omitempty"`
}
TraceView is the JSON projection of a Sample's trace: id, route, total, and the per-stage span breakdown. Served under /debug/tenant/{id} → "recent_traces".