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