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