Skip to content

Commit 43319be

Browse files
committed
feat(metrics): map forge histograms to cumulative prometheus buckets
1 parent a467b11 commit 43319be

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

internal/metrics/exporters/prometheus.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,51 @@ func (c *forgeCollector) emitComplex(ch chan<- prometheus.Metric, fqName string,
131131
}
132132
return
133133
}
134-
// Histogram / timer mapping is added in later tasks.
134+
135+
if raw, ok := v["buckets"].(map[float64]uint64); ok {
136+
c.emitHistogram(ch, fqName, v, raw, labels)
137+
return
138+
}
139+
// Timer / summary mapping is added in the next task.
140+
}
141+
142+
func (c *forgeCollector) emitHistogram(ch chan<- prometheus.Metric, fqName string,
143+
v map[string]any, perBucket map[float64]uint64, labels map[string]string) {
144+
bounds := make([]float64, 0, len(perBucket))
145+
for b := range perBucket {
146+
bounds = append(bounds, b)
147+
}
148+
sort.Float64s(bounds)
149+
150+
cumulative := make(map[float64]uint64, len(bounds))
151+
var running uint64
152+
for _, b := range bounds {
153+
running += perBucket[b]
154+
cumulative[b] = running
155+
}
156+
157+
count, _ := toUint64(v["count"])
158+
sum, _ := toFloat(v["sum"])
159+
160+
keys, vals := sortedLabels(labels)
161+
desc := prometheus.NewDesc(fqName, helpFor("histogram", fqName), keys, nil)
162+
ch <- prometheus.MustNewConstHistogram(desc, count, sum, cumulative, vals...)
163+
}
164+
165+
func toUint64(v any) (uint64, bool) {
166+
switch n := v.(type) {
167+
case uint64:
168+
return n, true
169+
case int64:
170+
if n >= 0 {
171+
return uint64(n), true
172+
}
173+
case float64:
174+
if n >= 0 {
175+
return uint64(n), true
176+
}
177+
}
178+
return 0, false
135179
}
136180

137181
// =============================================================================

internal/metrics/exporters/prometheus_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,35 @@ func TestBridge_GatherTextHasNoTimestamps(t *testing.T) {
4848
}
4949
}
5050
}
51+
52+
func TestBridge_Histogram(t *testing.T) {
53+
snapshot := func() map[string]any {
54+
return map[string]any{
55+
"request_latency_seconds": map[string]any{
56+
"count": uint64(6),
57+
"sum": float64(7.5),
58+
"buckets": map[float64]uint64{ // per-bucket (non-cumulative)
59+
0.1: 1,
60+
0.5: 2,
61+
1.0: 3,
62+
},
63+
},
64+
}
65+
}
66+
b := NewPrometheusBridge(snapshot, PrometheusConfig{})
67+
68+
expected := `
69+
# HELP request_latency_seconds Forge histogram request_latency_seconds
70+
# TYPE request_latency_seconds histogram
71+
request_latency_seconds_bucket{le="0.1"} 1
72+
request_latency_seconds_bucket{le="0.5"} 3
73+
request_latency_seconds_bucket{le="1"} 6
74+
request_latency_seconds_bucket{le="+Inf"} 6
75+
request_latency_seconds_sum 7.5
76+
request_latency_seconds_count 6
77+
`
78+
if err := testutil.CollectAndCompare(b.collector, strings.NewReader(expected),
79+
"request_latency_seconds"); err != nil {
80+
t.Fatalf("unexpected histogram exposition: %v", err)
81+
}
82+
}

0 commit comments

Comments
 (0)