Metrics Collector

Exercise: Metrics Collector

Difficulty - Intermediate

Learning Objectives

  • Implement metrics collection system
  • Track counters, gauges, and histograms
  • Aggregate metrics over time windows
  • Support metric labels and dimensions
  • Export metrics in Prometheus format

Problem Statement

Create a metrics collection system that tracks application metrics with support for different metric types and labels.

Core Components

 1package metrics
 2
 3import (
 4    "sync"
 5    "time"
 6)
 7
 8type MetricType int
 9
10const (
11    Counter MetricType = iota
12    Gauge
13    Histogram
14)
15
16type Metric struct {
17    Name      string
18    Type      MetricType
19    Value     float64
20    Labels    map[string]string
21    Timestamp time.Time
22}
23
24type Collector struct {
25    metrics map[string]*metricData
26    mu      sync.RWMutex
27}
28
29type metricData struct {
30    metricType MetricType
31    value      float64
32    samples    []float64
33    labels     map[string]string
34}
35
36func New() *Collector
37func Counter(name string, labels map[string]string) *Counter
38func Gauge(name string, labels map[string]string) *Gauge
39func Histogram(name string, labels map[string]string) *Histogram
40func Export() []Metric
41func Reset()

Solution

Click to see the solution
  1package metrics
  2
  3import (
  4    "fmt"
  5    "sort"
  6    "strings"
  7    "sync"
  8    "sync/atomic"
  9    "time"
 10)
 11
 12type MetricType int
 13
 14const (
 15    CounterType MetricType = iota
 16    GaugeType
 17    HistogramType
 18)
 19
 20type Metric struct {
 21    Name      string
 22    Type      MetricType
 23    Value     float64
 24    Labels    map[string]string
 25    Timestamp time.Time
 26}
 27
 28type Collector struct {
 29    metrics map[string]*metricData
 30    mu      sync.RWMutex
 31}
 32
 33type metricData struct {
 34    metricType MetricType
 35    value      uint64 // atomic
 36    samples    []float64
 37    labels     map[string]string
 38    mu         sync.Mutex
 39}
 40
 41type Counter struct {
 42    data *metricData
 43}
 44
 45type Gauge struct {
 46    data *metricData
 47}
 48
 49type Histogram struct {
 50    data *metricData
 51}
 52
 53func New() *Collector {
 54    return &Collector{
 55        metrics: make(map[string]*metricData),
 56    }
 57}
 58
 59func getOrCreate(name string, mtype MetricType, labels map[string]string) *metricData {
 60    key := metricKey(name, labels)
 61
 62    c.mu.RLock()
 63    if data, exists := c.metrics[key]; exists {
 64        c.mu.RUnlock()
 65        return data
 66    }
 67    c.mu.RUnlock()
 68
 69    c.mu.Lock()
 70    defer c.mu.Unlock()
 71
 72    if data, exists := c.metrics[key]; exists {
 73        return data
 74    }
 75
 76    data := &metricData{
 77        metricType: mtype,
 78        labels:     labels,
 79        samples:    make([]float64, 0),
 80    }
 81    c.metrics[key] = data
 82    return data
 83}
 84
 85func metricKey(name string, labels map[string]string) string {
 86    if len(labels) == 0 {
 87        return name
 88    }
 89
 90    keys := make([]string, 0, len(labels))
 91    for k := range labels {
 92        keys = append(keys, k)
 93    }
 94    sort.Strings(keys)
 95
 96    var sb strings.Builder
 97    sb.WriteString(name)
 98    sb.WriteString("{")
 99    for i, k := range keys {
100        if i > 0 {
101            sb.WriteString(",")
102        }
103        sb.WriteString(k)
104        sb.WriteString("=")
105        sb.WriteString(labels[k])
106    }
107    sb.WriteString("}")
108
109    return sb.String()
110}
111
112func Counter(name string, labels map[string]string) *Counter {
113    data := c.getOrCreate(name, CounterType, labels)
114    return &Counter{data: data}
115}
116
117func Gauge(name string, labels map[string]string) *Gauge {
118    data := c.getOrCreate(name, GaugeType, labels)
119    return &Gauge{data: data}
120}
121
122func Histogram(name string, labels map[string]string) *Histogram {
123    data := c.getOrCreate(name, HistogramType, labels)
124    return &Histogram{data: data}
125}
126
127func Inc() {
128    atomic.AddUint64(&c.data.value, 1)
129}
130
131func Add(delta float64) {
132    atomic.AddUint64(&c.data.value, uint64(delta))
133}
134
135func Value() float64 {
136    return float64(atomic.LoadUint64(&c.data.value))
137}
138
139func Set(value float64) {
140    atomic.StoreUint64(&g.data.value, uint64(value))
141}
142
143func Inc() {
144    atomic.AddUint64(&g.data.value, 1)
145}
146
147func Dec() {
148    atomic.AddUint64(&g.data.value, ^uint64(0)) // -1
149}
150
151func Value() float64 {
152    return float64(atomic.LoadUint64(&g.data.value))
153}
154
155func Observe(value float64) {
156    h.data.mu.Lock()
157    defer h.data.mu.Unlock()
158    h.data.samples = append(h.data.samples, value)
159}
160
161func Mean() float64 {
162    h.data.mu.Lock()
163    defer h.data.mu.Unlock()
164
165    if len(h.data.samples) == 0 {
166        return 0
167    }
168
169    sum := 0.0
170    for _, v := range h.data.samples {
171        sum += v
172    }
173
174    return sum / float64(len(h.data.samples))
175}
176
177func Percentile(p float64) float64 {
178    h.data.mu.Lock()
179    defer h.data.mu.Unlock()
180
181    if len(h.data.samples) == 0 {
182        return 0
183    }
184
185    sorted := make([]float64, len(h.data.samples))
186    copy(sorted, h.data.samples)
187    sort.Float64s(sorted)
188
189    idx := int(float64(len(sorted)) * p / 100.0)
190    if idx >= len(sorted) {
191        idx = len(sorted) - 1
192    }
193
194    return sorted[idx]
195}
196
197func Export() []Metric {
198    c.mu.RLock()
199    defer c.mu.RUnlock()
200
201    metrics := make([]Metric, 0, len(c.metrics))
202    now := time.Now()
203
204    for name, data := range c.metrics {
205        var value float64
206
207        switch data.metricType {
208        case CounterType, GaugeType:
209            value = float64(atomic.LoadUint64(&data.value))
210        case HistogramType:
211            data.mu.Lock()
212            if len(data.samples) > 0 {
213                sum := 0.0
214                for _, v := range data.samples {
215                    sum += v
216                }
217                value = sum / float64(len(data.samples))
218            }
219            data.mu.Unlock()
220        }
221
222        metrics = append(metrics, Metric{
223            Name:      name,
224            Type:      data.metricType,
225            Value:     value,
226            Labels:    data.labels,
227            Timestamp: now,
228        })
229    }
230
231    return metrics
232}
233
234func Reset() {
235    c.mu.Lock()
236    defer c.mu.Unlock()
237    c.metrics = make(map[string]*metricData)
238}
239
240func String() string {
241    metrics := c.Export()
242    var sb strings.Builder
243
244    for _, m := range metrics {
245        sb.WriteString(fmt.Sprintf("%s{", m.Name))
246        first := true
247        for k, v := range m.Labels {
248            if !first {
249                sb.WriteString(",")
250            }
251            sb.WriteString(fmt.Sprintf("%s=%q", k, v))
252            first = false
253        }
254        sb.WriteString(fmt.Sprintf("} %.2f\n", m.Value))
255    }
256
257    return sb.String()
258}

Key Takeaways

  • Metrics enable observability
  • Counters track cumulative values
  • Gauges track current values
  • Histograms track distributions
  • Labels add dimensions to metrics
  • Atomic operations ensure thread safety