// Package metrics provides bucketed timing histograms for the relay's // hot paths. Single-threaded per domain — no locking. Each instrumented // site Observe()s a duration in nanoseconds; Snapshot() emits JSON for // the /metrics endpoint or test scrapers. // // Bucket layout: 25 power-of-2 buckets covering 0 ns to ~16 s. Each // bucket i covers [2^i, 2^(i+1)) ns, with bucket 0 = [0, 2) ns and // bucket 24 capturing every observation ≥ 2^24 ns (~16 ms) in its // upper-bound percentile output. package metrics import ( "time" ) const numBuckets = 25 // Histogram is a power-of-2 bucketed histogram of nanosecond durations. type Histogram struct { Name string Buckets [numBuckets]uint64 Count uint64 SumNs uint64 } // NewHistogram creates a histogram with the given identifier. func NewHistogram(name string) *Histogram { return &Histogram{Name: name} } // Observe records a duration in nanoseconds. func (h *Histogram) Observe(ns int64) { if ns < 0 { ns = 0 } h.Count++ h.SumNs += uint64(ns) b := bucketOf(uint64(ns)) h.Buckets[b]++ } // Reset clears all counters. Useful between test runs. func (h *Histogram) Reset() { h.Count = 0 h.SumNs = 0 for i := 0; i < numBuckets; i++ { h.Buckets[i] = 0 } } // bucketOf returns the bucket index for a given ns value. func bucketOf(ns uint64) int { if ns < 2 { return 0 } b := 0 for ns >= 2 && b < numBuckets-1 { ns >>= 1 b++ } return b } // Percentile returns the upper bound (in ns) of the bucket holding the // p-th percentile sample. Approximate within bucket width. func (h *Histogram) Percentile(p int) uint64 { if h.Count == 0 { return 0 } target := h.Count * uint64(p) / 100 if target == 0 { target = 1 } var seen uint64 for i := 0; i < numBuckets; i++ { seen += h.Buckets[i] if seen >= target { return uint64(1) << uint(i+1) } } return uint64(1) << numBuckets } // AvgNs returns the arithmetic mean observation in ns, or 0 if no // observations yet. func (h *Histogram) AvgNs() uint64 { if h.Count == 0 { return 0 } return h.SumNs / h.Count } // AppendJSON writes a JSON object for this histogram to buf. func (h *Histogram) AppendJSON(buf []byte) []byte { buf = append(buf, '"') buf = append(buf, h.Name...) buf = append(buf, "\":{\"count\":"...) buf = appendUint64(buf, h.Count) buf = append(buf, ",\"sum_ns\":"...) buf = appendUint64(buf, h.SumNs) buf = append(buf, ",\"avg_ns\":"...) buf = appendUint64(buf, h.AvgNs()) buf = append(buf, ",\"p50_ns\":"...) buf = appendUint64(buf, h.Percentile(50)) buf = append(buf, ",\"p95_ns\":"...) buf = appendUint64(buf, h.Percentile(95)) buf = append(buf, ",\"p99_ns\":"...) buf = appendUint64(buf, h.Percentile(99)) buf = append(buf, ",\"buckets\":["...) for i := 0; i < numBuckets; i++ { if i > 0 { buf = append(buf, ',') } buf = appendUint64(buf, h.Buckets[i]) } buf = append(buf, ']') buf = append(buf, '}') return buf } // Now returns the current monotonic nanosecond timestamp. func Now() int64 { return time.Now().UnixNano() } // Since returns ns elapsed since start. func Since(start int64) int64 { return Now() - start } // appendUint64 writes a base-10 encoding of n into buf without using // fmt (which would pull in heavy dependencies into the hot path). func appendUint64(buf []byte, n uint64) []byte { if n == 0 { return append(buf, '0') } var tmp [20]byte i := 20 for n > 0 { i-- tmp[i] = byte('0' + n%10) n /= 10 } return append(buf, tmp[i:]...) }