Skip to content

Commit 2a72155

Browse files
committed
Emit query latency metrics
Add a query duration helper and instrument source/target query paths with millisecond latency histograms. Metrics emitted: - query.duration_milliseconds tagged with side, kind, and outcome Initial coverage includes chunk copy, binlog apply, range select, and exact row count queries. This lets dashboards compare source and target query latency side-by-side and break down latency by query kind. Add unit coverage for the query duration metric helper.
1 parent 475456b commit 2a72155

4 files changed

Lines changed: 73 additions & 0 deletions

File tree

go/logic/applier.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/github/gh-ost/go/base"
1818
"github.com/github/gh-ost/go/binlog"
19+
"github.com/github/gh-ost/go/metrics"
1920
"github.com/github/gh-ost/go/sql"
2021

2122
"context"
@@ -893,22 +894,27 @@ func (apl *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange bool
893894
return hasFurtherRange, err
894895
}
895896

897+
queryStartTime := time.Now()
896898
rows, err := apl.db.Query(query, explodedArgs...)
897899
if err != nil {
900+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "source", "range_select", time.Since(queryStartTime), err)
898901
return hasFurtherRange, err
899902
}
900903
defer rows.Close()
901904

902905
iterationRangeMaxValues := sql.NewColumnValues(apl.migrationContext.UniqueKey.Len())
903906
for rows.Next() {
904907
if err = rows.Scan(iterationRangeMaxValues.ValuesPointers...); err != nil {
908+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "source", "range_select", time.Since(queryStartTime), err)
905909
return hasFurtherRange, err
906910
}
907911
hasFurtherRange = true
908912
}
909913
if err = rows.Err(); err != nil {
914+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "source", "range_select", time.Since(queryStartTime), err)
910915
return hasFurtherRange, err
911916
}
917+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "source", "range_select", time.Since(queryStartTime), nil)
912918
if hasFurtherRange {
913919
apl.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
914920
return hasFurtherRange, nil
@@ -956,7 +962,9 @@ func (apl *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected i
956962
if _, err := tx.Exec(sessionQuery); err != nil {
957963
return nil, err
958964
}
965+
queryStartTime := time.Now()
959966
result, err := tx.Exec(query, explodedArgs...)
967+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "target", "chunk_copy", time.Since(queryStartTime), err)
960968
if err != nil {
961969
return nil, err
962970
}
@@ -1669,13 +1677,16 @@ func (apl *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent)) e
16691677
// in the batch. SHOW WARNINGS only shows warnings from the last statement in a
16701678
// multi-statement query, so we interleave SHOW WARNINGS after each DML statement.
16711679
if apl.migrationContext.PanicOnWarnings {
1680+
queryStartTime := time.Now()
16721681
totalDelta, err = apl.executeBatchWithWarningChecking(ctx, tx, buildResults)
1682+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "target", "binlog_apply", time.Since(queryStartTime), err)
16731683
if err != nil {
16741684
return rollback(err)
16751685
}
16761686
} else {
16771687
// Fast path: batch together DML queries into multi-statements to minimize network trips.
16781688
// We use the raw driver connection to access the rows affected for each statement.
1689+
queryStartTime := time.Now()
16791690
execErr := conn.Raw(func(driverConn any) error {
16801691
ex := driverConn.(driver.ExecerContext)
16811692
nvc := driverConn.(driver.NamedValueChecker)
@@ -1709,6 +1720,7 @@ func (apl *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent)) e
17091720
return nil
17101721
})
17111722

1723+
metrics.RecordQueryDuration(apl.migrationContext.Metrics, "target", "binlog_apply", time.Since(queryStartTime), execErr)
17121724
if execErr != nil {
17131725
return rollback(execErr)
17141726
}

go/logic/inspect.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"github.com/github/gh-ost/go/base"
19+
"github.com/github/gh-ost/go/metrics"
1920
"github.com/github/gh-ost/go/mysql"
2021
"github.com/github/gh-ost/go/sql"
2122

@@ -677,13 +678,16 @@ func (isp *Inspector) CountTableRows(ctx context.Context) error {
677678

678679
query := fmt.Sprintf(`select /* gh-ost */ count(*) as count_rows from %s.%s`, sql.EscapeName(isp.migrationContext.DatabaseName), sql.EscapeName(isp.migrationContext.OriginalTableName))
679680
var rowsEstimate int64
681+
queryStartTime := time.Now()
680682
if err := conn.QueryRowContext(ctx, query).Scan(&rowsEstimate); err != nil {
683+
metrics.RecordQueryDuration(isp.migrationContext.Metrics, "source", "row_count", time.Since(queryStartTime), err)
681684
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
682685
isp.migrationContext.Log.Infof("exact row count cancelled (%s), likely because I'm about to cut over. I'm going to kill that query.", ctx.Err())
683686
return mysql.Kill(isp.db, connectionID)
684687
}
685688
return err
686689
}
690+
metrics.RecordQueryDuration(isp.migrationContext.Metrics, "source", "row_count", time.Since(queryStartTime), nil)
687691

688692
// row count query finished. nil out the cancel func, so the main migration thread
689693
// doesn't bother calling it after row copy is done.

go/metrics/emit.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,15 @@ func EmitThrottleInterval(emit Emitter, duration time.Duration, reason string) {
129129
emit.Histogram("throttle.duration_seconds", duration.Seconds(), tags...)
130130
emit.Count("throttle.events_total", 1, tags...)
131131
}
132+
133+
// RecordQueryDuration emits gh_ost.query.duration_milliseconds with side/kind/outcome tags.
134+
func RecordQueryDuration(emit Emitter, side string, kind string, duration time.Duration, err error) {
135+
if emit == nil || side == "" || kind == "" || duration < 0 {
136+
return
137+
}
138+
outcome := "ok"
139+
if err != nil {
140+
outcome = "error"
141+
}
142+
emit.Histogram("query.duration_milliseconds", float64(duration.Milliseconds()), "side:"+side, "kind:"+kind, "outcome:"+outcome)
143+
}

go/metrics/emit_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package metrics
77

88
import (
99
"context"
10+
"errors"
1011
"runtime"
12+
"slices"
1113
"testing"
1214
"time"
1315
)
@@ -256,3 +258,46 @@ func TestEmitThrottleIntervalNilSafe(t *testing.T) {
256258
EmitThrottleInterval(nil, time.Second, "test")
257259
EmitThrottleInterval(&gaugeSpy{}, time.Second, "test")
258260
}
261+
262+
type histogramSpy struct {
263+
names []string
264+
values []float64
265+
tags [][]string
266+
}
267+
268+
func (h *histogramSpy) Gauge(_ string, _ float64, _ ...string) {}
269+
270+
func (h *histogramSpy) Count(_ string, _ int64, _ ...string) {}
271+
272+
func (h *histogramSpy) Histogram(name string, value float64, tags ...string) {
273+
h.names = append(h.names, name)
274+
h.values = append(h.values, value)
275+
h.tags = append(h.tags, tags)
276+
}
277+
278+
func TestRecordQueryDuration(t *testing.T) {
279+
spy := &histogramSpy{}
280+
281+
RecordQueryDuration(spy, "source", "row_count", 1500*time.Millisecond, nil)
282+
RecordQueryDuration(spy, "target", "binlog_apply", 2*time.Second, errors.New("boom"))
283+
284+
if len(spy.names) != 2 {
285+
t.Fatalf("got %d histograms, want 2", len(spy.names))
286+
}
287+
if spy.names[0] != "query.duration_milliseconds" || spy.values[0] != 1500 {
288+
t.Fatalf("got %s=%v, want query.duration_milliseconds=1500", spy.names[0], spy.values[0])
289+
}
290+
if !slices.Equal(spy.tags[0], []string{"side:source", "kind:row_count", "outcome:ok"}) {
291+
t.Fatalf("got tags %#v", spy.tags[0])
292+
}
293+
if spy.values[1] != 2000 || !slices.Equal(spy.tags[1], []string{"side:target", "kind:binlog_apply", "outcome:error"}) {
294+
t.Fatalf("got second metric value=%v tags=%#v", spy.values[1], spy.tags[1])
295+
}
296+
}
297+
298+
func TestRecordQueryDurationNilSafe(t *testing.T) {
299+
RecordQueryDuration(nil, "source", "row_count", time.Second, nil)
300+
RecordQueryDuration(&histogramSpy{}, "", "row_count", time.Second, nil)
301+
RecordQueryDuration(&histogramSpy{}, "source", "", time.Second, nil)
302+
RecordQueryDuration(&histogramSpy{}, "source", "row_count", -time.Second, nil)
303+
}

0 commit comments

Comments
 (0)