Skip to content

Commit aa3774f

Browse files
committed
Consolidate metrics emit helpers
Keep the small metric emission helpers and their tests together so the metrics package has fewer one-function files.
1 parent 9b8cb09 commit aa3774f

10 files changed

Lines changed: 250 additions & 273 deletions

File tree

go/metrics/binlog_backlog.go

Lines changed: 0 additions & 31 deletions
This file was deleted.

go/metrics/binlog_backlog_test.go

Lines changed: 0 additions & 53 deletions
This file was deleted.

go/metrics/emit.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright 2026 GitHub Inc.
3+
See https://github.com/github/gh-ost/blob/master/LICENSE
4+
*/
5+
6+
package metrics
7+
8+
import (
9+
"fmt"
10+
"runtime"
11+
)
12+
13+
// EmitProgressGauges emits row-copy and DML progress gauges (namespace is applied by the client):
14+
// gh_ost.row_copy.rows_copied, gh_ost.row_copy.rows_estimate, gh_ost.dml.events_applied.
15+
func EmitProgressGauges(emit Emitter, rowsCopied, rowsEstimate, dmlEventsApplied int64) {
16+
if emit == nil {
17+
return
18+
}
19+
emit.Gauge("row_copy.rows_copied", float64(rowsCopied))
20+
emit.Gauge("row_copy.rows_estimate", float64(rowsEstimate))
21+
emit.Gauge("dml.events_applied", float64(dmlEventsApplied))
22+
}
23+
24+
// EmitBinlogBacklogGauges emits apply-events queue depth gauges (namespace is applied by the client):
25+
// gh_ost.binlog.backlog_size, gh_ost.binlog.backlog_capacity, gh_ost.binlog.backlog_utilization.
26+
func EmitBinlogBacklogGauges(emit Emitter, backlogSize, backlogCapacity int) {
27+
if emit == nil {
28+
return
29+
}
30+
emit.Gauge("binlog.backlog_size", float64(backlogSize))
31+
emit.Gauge("binlog.backlog_capacity", float64(backlogCapacity))
32+
emit.Gauge("binlog.backlog_utilization", binlogBacklogUtilization(backlogSize, backlogCapacity))
33+
}
34+
35+
func binlogBacklogUtilization(backlogSize, backlogCapacity int) float64 {
36+
if backlogCapacity <= 0 {
37+
return 0
38+
}
39+
utilization := float64(backlogSize) / float64(backlogCapacity)
40+
if utilization > 1 {
41+
return 1
42+
}
43+
if utilization < 0 {
44+
return 0
45+
}
46+
return utilization
47+
}
48+
49+
// EmitLagGauges emits replication and heartbeat lag gauges (namespace is applied by the client):
50+
// gh_ost.lag.replication_seconds, gh_ost.lag.heartbeat_seconds, each tagged throttled:true|false.
51+
//
52+
// These are point-in-time readings each status tick (not a distribution), so gauges are used
53+
// rather than histograms; DogStatsD histogram aggregation exposes count/max series that do not
54+
// match the log line lag values in Prometheus/Grafana.
55+
func EmitLagGauges(emit Emitter, replicationLagSeconds, heartbeatLagSeconds float64, throttled bool) {
56+
if emit == nil {
57+
return
58+
}
59+
tags := []string{fmt.Sprintf("throttled:%t", throttled)}
60+
emit.Gauge("lag.replication_seconds", replicationLagSeconds, tags...)
61+
emit.Gauge("lag.heartbeat_seconds", heartbeatLagSeconds, tags...)
62+
}
63+
64+
// EmitGoRuntimeGauges emits gh_ost.go_runtime.* gauges (namespace is applied by the client).
65+
// m and numGoroutine are typically from runtime.ReadMemStats and runtime.NumGoroutine.
66+
func EmitGoRuntimeGauges(emit Emitter, m *runtime.MemStats, numGoroutine int) {
67+
if emit == nil || m == nil {
68+
return
69+
}
70+
emit.Gauge("go_runtime.alloc_bytes", float64(m.Alloc))
71+
emit.Gauge("go_runtime.sys_bytes", float64(m.Sys))
72+
emit.Gauge("go_runtime.heap_inuse_bytes", float64(m.HeapInuse))
73+
emit.Gauge("go_runtime.num_gc", float64(m.NumGC))
74+
emit.Gauge("go_runtime.gc_pause_total_ns", float64(m.PauseTotalNs))
75+
emit.Gauge("go_runtime.goroutines", float64(numGoroutine))
76+
}

go/metrics/emit_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
Copyright 2026 GitHub Inc.
3+
See https://github.com/github/gh-ost/blob/master/LICENSE
4+
*/
5+
6+
package metrics
7+
8+
import (
9+
"runtime"
10+
"testing"
11+
)
12+
13+
type gaugeSpy struct {
14+
names []string
15+
values []float64
16+
tags [][]string
17+
}
18+
19+
func (g *gaugeSpy) Gauge(name string, value float64, tags ...string) {
20+
g.names = append(g.names, name)
21+
g.values = append(g.values, value)
22+
g.tags = append(g.tags, append([]string(nil), tags...))
23+
}
24+
25+
func (g *gaugeSpy) Count(name string, value int64, tags ...string) {
26+
}
27+
28+
func (g *gaugeSpy) Histogram(name string, value float64, tags ...string) {
29+
}
30+
31+
func TestEmitProgressGauges(t *testing.T) {
32+
spy := &gaugeSpy{}
33+
EmitProgressGauges(spy, 1000, 5000, 42)
34+
35+
wantNames := []string{
36+
"row_copy.rows_copied",
37+
"row_copy.rows_estimate",
38+
"dml.events_applied",
39+
}
40+
wantVals := []float64{1000, 5000, 42}
41+
42+
if len(spy.names) != len(wantNames) {
43+
t.Fatalf("got %d gauges, want %d", len(spy.names), len(wantNames))
44+
}
45+
for i := range wantNames {
46+
if spy.names[i] != wantNames[i] || spy.values[i] != wantVals[i] {
47+
t.Fatalf("[%d] got %s=%v want %s=%v", i, spy.names[i], spy.values[i], wantNames[i], wantVals[i])
48+
}
49+
}
50+
}
51+
52+
func TestEmitProgressGauges_nilSafe(t *testing.T) {
53+
EmitProgressGauges(nil, 1, 2, 3)
54+
}
55+
56+
func TestEmitBinlogBacklogGauges(t *testing.T) {
57+
spy := &gaugeSpy{}
58+
EmitBinlogBacklogGauges(spy, 250, 1000)
59+
60+
wantNames := []string{
61+
"binlog.backlog_size",
62+
"binlog.backlog_capacity",
63+
"binlog.backlog_utilization",
64+
}
65+
wantVals := []float64{250, 1000, 0.25}
66+
67+
if len(spy.names) != len(wantNames) {
68+
t.Fatalf("got %d gauges, want %d", len(spy.names), len(wantNames))
69+
}
70+
for i := range wantNames {
71+
if spy.names[i] != wantNames[i] || spy.values[i] != wantVals[i] {
72+
t.Fatalf("[%d] got %s=%v want %s=%v", i, spy.names[i], spy.values[i], wantNames[i], wantVals[i])
73+
}
74+
}
75+
}
76+
77+
func TestEmitBinlogBacklogGauges_nilSafe(t *testing.T) {
78+
EmitBinlogBacklogGauges(nil, 1, 2)
79+
}
80+
81+
func TestBinlogBacklogUtilization(t *testing.T) {
82+
tests := []struct {
83+
size, capacity int
84+
want float64
85+
}{
86+
{0, 1000, 0},
87+
{250, 1000, 0.25},
88+
{1000, 1000, 1},
89+
{1500, 1000, 1},
90+
{-1, 1000, 0},
91+
{10, 0, 0},
92+
}
93+
for _, tt := range tests {
94+
got := binlogBacklogUtilization(tt.size, tt.capacity)
95+
if got != tt.want {
96+
t.Fatalf("utilization(%d, %d) = %v, want %v", tt.size, tt.capacity, got, tt.want)
97+
}
98+
}
99+
}
100+
101+
func TestEmitLagGauges_notThrottled(t *testing.T) {
102+
spy := &gaugeSpy{}
103+
EmitLagGauges(spy, 2.5, 1.25, false)
104+
105+
wantNames := []string{"lag.replication_seconds", "lag.heartbeat_seconds"}
106+
wantVals := []float64{2.5, 1.25}
107+
wantTags := []string{"throttled:false"}
108+
109+
if len(spy.names) != len(wantNames) {
110+
t.Fatalf("got %d gauges, want %d", len(spy.names), len(wantNames))
111+
}
112+
for i := range wantNames {
113+
if spy.names[i] != wantNames[i] || spy.values[i] != wantVals[i] {
114+
t.Fatalf("[%d] got %s=%v want %s=%v", i, spy.names[i], spy.values[i], wantNames[i], wantVals[i])
115+
}
116+
if len(spy.tags[i]) != 1 || spy.tags[i][0] != wantTags[0] {
117+
t.Fatalf("[%d] got tags %v want [%s]", i, spy.tags[i], wantTags[0])
118+
}
119+
}
120+
}
121+
122+
func TestEmitLagGauges_throttled(t *testing.T) {
123+
spy := &gaugeSpy{}
124+
EmitLagGauges(spy, 4.0, 3.0, true)
125+
126+
if len(spy.names) != 2 {
127+
t.Fatalf("got %d gauges, want 2", len(spy.names))
128+
}
129+
for i := range spy.names {
130+
if len(spy.tags[i]) != 1 || spy.tags[i][0] != "throttled:true" {
131+
t.Fatalf("[%d] got tags %v want [throttled:true]", i, spy.tags[i])
132+
}
133+
}
134+
}
135+
136+
func TestEmitLagGauges_nilSafe(t *testing.T) {
137+
EmitLagGauges(nil, 1, 2, false)
138+
}
139+
140+
func TestEmitGoRuntimeGauges(t *testing.T) {
141+
spy := &gaugeSpy{}
142+
m := &runtime.MemStats{
143+
Alloc: 100,
144+
Sys: 200,
145+
HeapInuse: 300,
146+
NumGC: 7,
147+
PauseTotalNs: 42,
148+
}
149+
EmitGoRuntimeGauges(spy, m, 123)
150+
151+
wantNames := []string{
152+
"go_runtime.alloc_bytes",
153+
"go_runtime.sys_bytes",
154+
"go_runtime.heap_inuse_bytes",
155+
"go_runtime.num_gc",
156+
"go_runtime.gc_pause_total_ns",
157+
"go_runtime.goroutines",
158+
}
159+
wantVals := []float64{100, 200, 300, 7, 42, 123}
160+
161+
if len(spy.names) != len(wantNames) {
162+
t.Fatalf("got %d gauges, want %d", len(spy.names), len(wantNames))
163+
}
164+
for i := range wantNames {
165+
if spy.names[i] != wantNames[i] || spy.values[i] != wantVals[i] {
166+
t.Fatalf("[%d] got %s=%v want %s=%v", i, spy.names[i], spy.values[i], wantNames[i], wantVals[i])
167+
}
168+
}
169+
}
170+
171+
func TestEmitGoRuntimeGauges_nilSafe(t *testing.T) {
172+
EmitGoRuntimeGauges(nil, &runtime.MemStats{}, 1)
173+
EmitGoRuntimeGauges(&gaugeSpy{}, nil, 1)
174+
}

go/metrics/go_runtime.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,6 @@ import (
1111
"time"
1212
)
1313

14-
// EmitGoRuntimeGauges emits gh_ost.go_runtime.* gauges (namespace is applied by the client).
15-
// m and numGoroutine are typically from runtime.ReadMemStats and runtime.NumGoroutine.
16-
func EmitGoRuntimeGauges(emit Emitter, m *runtime.MemStats, numGoroutine int) {
17-
if emit == nil || m == nil {
18-
return
19-
}
20-
emit.Gauge("go_runtime.alloc_bytes", float64(m.Alloc))
21-
emit.Gauge("go_runtime.sys_bytes", float64(m.Sys))
22-
emit.Gauge("go_runtime.heap_inuse_bytes", float64(m.HeapInuse))
23-
emit.Gauge("go_runtime.num_gc", float64(m.NumGC))
24-
emit.Gauge("go_runtime.gc_pause_total_ns", float64(m.PauseTotalNs))
25-
emit.Gauge("go_runtime.goroutines", float64(numGoroutine))
26-
}
27-
2814
// StartGoRuntimeReporter periodically samples runtime memory and goroutines and emits gauges
2915
// until ctx is cancelled. It is a no-op when interval <= 0, client is nil, or StatsD is disabled
3016
// (noop client).

0 commit comments

Comments
 (0)