Skip to content

Commit 71ac9c6

Browse files
authored
Merge pull request #32 from Shopify/statsd-gometrics
Add go runtime metrics to statsd reporting
2 parents be3853c + 141ea12 commit 71ac9c6

3 files changed

Lines changed: 133 additions & 0 deletions

File tree

go/cmd/gh-ost/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os/signal"
1414
"regexp"
1515
"syscall"
16+
"time"
1617

1718
"github.com/github/gh-ost/go/base"
1819
"github.com/github/gh-ost/go/logic"
@@ -174,6 +175,7 @@ func main() {
174175
statsdAddr := flag.String("statsd-addr", "", "StatsD endpoint (host:port or unix socket); empty disables StatsD")
175176
var statsdTags statsdTagList
176177
flag.Var(&statsdTags, "statsd-tags", "global StatsD tags applied to every metric (repeatable), format key:value. Example: --statsd-tags 'env:prod,service:my-service'")
178+
runtimeMetricsInterval := flag.Int("runtime-metrics-interval", 10, "Seconds between Go runtime memory/GC gauge samples (requires --statsd-addr); 0 disables")
177179
quiet := flag.Bool("quiet", false, "quiet")
178180
verbose := flag.Bool("verbose", false, "verbose")
179181
debug := flag.Bool("debug", false, "debug mode (very verbose)")
@@ -400,6 +402,9 @@ func main() {
400402
defer func() { _ = metricsClient.Close() }()
401403
migrationContext.Metrics = metricsClient
402404
metricsClient.Count("startup", 1)
405+
if *runtimeMetricsInterval > 0 {
406+
metrics.StartGoRuntimeReporter(migrationContext.GetContext(), metricsClient, time.Duration(*runtimeMetricsInterval)*time.Second)
407+
}
403408

404409
migrator := logic.NewMigrator(migrationContext, AppVersion)
405410
var err error

go/metrics/go_runtime.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2022 GitHub Inc.
3+
See https://github.com/github/gh-ost/blob/master/LICENSE
4+
*/
5+
6+
package metrics
7+
8+
import (
9+
"context"
10+
"runtime"
11+
"time"
12+
)
13+
14+
// MemStatsGaugeEmitter is implemented by *Client; used for tests without UDP.
15+
type MemStatsGaugeEmitter interface {
16+
Gauge(name string, value float64, tags ...string)
17+
}
18+
19+
// EmitGoRuntimeGauges emits gh_ost.go_runtime.* gauges (namespace is applied by the client).
20+
// m and numGoroutine are typically from runtime.ReadMemStats and runtime.NumGoroutine.
21+
func EmitGoRuntimeGauges(emit MemStatsGaugeEmitter, m *runtime.MemStats, numGoroutine int) {
22+
if emit == nil || m == nil {
23+
return
24+
}
25+
emit.Gauge("go_runtime.alloc_bytes", float64(m.Alloc))
26+
emit.Gauge("go_runtime.sys_bytes", float64(m.Sys))
27+
emit.Gauge("go_runtime.heap_inuse_bytes", float64(m.HeapInuse))
28+
emit.Gauge("go_runtime.num_gc", float64(m.NumGC))
29+
emit.Gauge("go_runtime.gc_pause_total_ns", float64(m.PauseTotalNs))
30+
emit.Gauge("go_runtime.goroutines", float64(numGoroutine))
31+
}
32+
33+
// StartGoRuntimeReporter periodically samples runtime memory and goroutines and emits gauges
34+
// until ctx is cancelled. It is a no-op when interval <= 0, client is nil, or StatsD is disabled
35+
// (noop client).
36+
func StartGoRuntimeReporter(ctx context.Context, client *Client, interval time.Duration) {
37+
if ctx == nil || client == nil || interval <= 0 || client.sd == nil {
38+
return
39+
}
40+
41+
emit := func() {
42+
var m runtime.MemStats
43+
runtime.ReadMemStats(&m)
44+
EmitGoRuntimeGauges(client, &m, runtime.NumGoroutine())
45+
}
46+
47+
go func() {
48+
ticker := time.NewTicker(interval)
49+
defer ticker.Stop()
50+
51+
emit()
52+
for {
53+
select {
54+
case <-ctx.Done():
55+
return
56+
case <-ticker.C:
57+
emit()
58+
}
59+
}
60+
}()
61+
}

go/metrics/go_runtime_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2022 GitHub Inc.
3+
See https://github.com/github/gh-ost/blob/master/LICENSE
4+
*/
5+
6+
package metrics
7+
8+
import (
9+
"context"
10+
"runtime"
11+
"testing"
12+
"time"
13+
)
14+
15+
type gaugeSpy struct {
16+
names []string
17+
values []float64
18+
}
19+
20+
func (g *gaugeSpy) Gauge(name string, value float64, _ ...string) {
21+
g.names = append(g.names, name)
22+
g.values = append(g.values, value)
23+
}
24+
25+
func TestEmitGoRuntimeGauges(t *testing.T) {
26+
spy := &gaugeSpy{}
27+
m := &runtime.MemStats{
28+
Alloc: 100,
29+
Sys: 200,
30+
HeapInuse: 300,
31+
NumGC: 7,
32+
PauseTotalNs: 42,
33+
}
34+
EmitGoRuntimeGauges(spy, m, 123)
35+
36+
wantNames := []string{
37+
"go_runtime.alloc_bytes",
38+
"go_runtime.sys_bytes",
39+
"go_runtime.heap_inuse_bytes",
40+
"go_runtime.num_gc",
41+
"go_runtime.gc_pause_total_ns",
42+
"go_runtime.goroutines",
43+
}
44+
wantVals := []float64{100, 200, 300, 7, 42, 123}
45+
46+
if len(spy.names) != len(wantNames) {
47+
t.Fatalf("got %d gauges, want %d", len(spy.names), len(wantNames))
48+
}
49+
for i := range wantNames {
50+
if spy.names[i] != wantNames[i] || spy.values[i] != wantVals[i] {
51+
t.Fatalf("[%d] got %s=%v want %s=%v", i, spy.names[i], spy.values[i], wantNames[i], wantVals[i])
52+
}
53+
}
54+
}
55+
56+
func TestEmitGoRuntimeGauges_nilSafe(t *testing.T) {
57+
EmitGoRuntimeGauges(nil, &runtime.MemStats{}, 1)
58+
EmitGoRuntimeGauges(&gaugeSpy{}, nil, 1)
59+
}
60+
61+
func TestStartGoRuntimeReporter_stopsOnCancel(t *testing.T) {
62+
ctx, cancel := context.WithCancel(context.Background())
63+
c := &Client{} // sd nil — should not start
64+
StartGoRuntimeReporter(ctx, c, time.Millisecond)
65+
cancel()
66+
time.Sleep(20 * time.Millisecond)
67+
}

0 commit comments

Comments
 (0)