metrics.mx raw
1 // Package metrics provides bucketed timing histograms for the relay's
2 // hot paths. Single-threaded per domain — no locking. Each instrumented
3 // site Observe()s a duration in nanoseconds; Snapshot() emits JSON for
4 // the /metrics endpoint or test scrapers.
5 //
6 // Bucket layout: 25 power-of-2 buckets covering 0 ns to ~16 s. Each
7 // bucket i covers [2^i, 2^(i+1)) ns, with bucket 0 = [0, 2) ns and
8 // bucket 24 capturing every observation ≥ 2^24 ns (~16 ms) in its
9 // upper-bound percentile output.
10 package metrics
11
12 import (
13 "time"
14 )
15
16 const numBuckets = 25
17
18 // Histogram is a power-of-2 bucketed histogram of nanosecond durations.
19 type Histogram struct {
20 Name string
21 Buckets [numBuckets]uint64
22 Count uint64
23 SumNs uint64
24 }
25
26 // NewHistogram creates a histogram with the given identifier.
27 func NewHistogram(name string) *Histogram {
28 return &Histogram{Name: name}
29 }
30
31 // Observe records a duration in nanoseconds.
32 func (h *Histogram) Observe(ns int64) {
33 if ns < 0 {
34 ns = 0
35 }
36 h.Count++
37 h.SumNs += uint64(ns)
38 b := bucketOf(uint64(ns))
39 h.Buckets[b]++
40 }
41
42 // Reset clears all counters. Useful between test runs.
43 func (h *Histogram) Reset() {
44 h.Count = 0
45 h.SumNs = 0
46 for i := 0; i < numBuckets; i++ {
47 h.Buckets[i] = 0
48 }
49 }
50
51 // bucketOf returns the bucket index for a given ns value.
52 func bucketOf(ns uint64) int {
53 if ns < 2 {
54 return 0
55 }
56 b := 0
57 for ns >= 2 && b < numBuckets-1 {
58 ns >>= 1
59 b++
60 }
61 return b
62 }
63
64 // Percentile returns the upper bound (in ns) of the bucket holding the
65 // p-th percentile sample. Approximate within bucket width.
66 func (h *Histogram) Percentile(p int) uint64 {
67 if h.Count == 0 {
68 return 0
69 }
70 target := h.Count * uint64(p) / 100
71 if target == 0 {
72 target = 1
73 }
74 var seen uint64
75 for i := 0; i < numBuckets; i++ {
76 seen += h.Buckets[i]
77 if seen >= target {
78 return uint64(1) << uint(i+1)
79 }
80 }
81 return uint64(1) << numBuckets
82 }
83
84 // AvgNs returns the arithmetic mean observation in ns, or 0 if no
85 // observations yet.
86 func (h *Histogram) AvgNs() uint64 {
87 if h.Count == 0 {
88 return 0
89 }
90 return h.SumNs / h.Count
91 }
92
93 // AppendJSON writes a JSON object for this histogram to buf.
94 func (h *Histogram) AppendJSON(buf []byte) []byte {
95 buf = append(buf, '"')
96 buf = append(buf, h.Name...)
97 buf = append(buf, "\":{\"count\":"...)
98 buf = appendUint64(buf, h.Count)
99 buf = append(buf, ",\"sum_ns\":"...)
100 buf = appendUint64(buf, h.SumNs)
101 buf = append(buf, ",\"avg_ns\":"...)
102 buf = appendUint64(buf, h.AvgNs())
103 buf = append(buf, ",\"p50_ns\":"...)
104 buf = appendUint64(buf, h.Percentile(50))
105 buf = append(buf, ",\"p95_ns\":"...)
106 buf = appendUint64(buf, h.Percentile(95))
107 buf = append(buf, ",\"p99_ns\":"...)
108 buf = appendUint64(buf, h.Percentile(99))
109 buf = append(buf, ",\"buckets\":["...)
110 for i := 0; i < numBuckets; i++ {
111 if i > 0 {
112 buf = append(buf, ',')
113 }
114 buf = appendUint64(buf, h.Buckets[i])
115 }
116 buf = append(buf, ']')
117 buf = append(buf, '}')
118 return buf
119 }
120
121 // Now returns the current monotonic nanosecond timestamp.
122 func Now() int64 {
123 return time.Now().UnixNano()
124 }
125
126 // Since returns ns elapsed since start.
127 func Since(start int64) int64 {
128 return Now() - start
129 }
130
131 // appendUint64 writes a base-10 encoding of n into buf without using
132 // fmt (which would pull in heavy dependencies into the hot path).
133 func appendUint64(buf []byte, n uint64) []byte {
134 if n == 0 {
135 return append(buf, '0')
136 }
137 var tmp [20]byte
138 i := 20
139 for n > 0 {
140 i--
141 tmp[i] = byte('0' + n%10)
142 n /= 10
143 }
144 return append(buf, tmp[i:]...)
145 }
146