Skip to content

Commit 8015c79

Browse files
authored
Merge pull request #46 from Shopify/grodowski/issue-5458-query-latency-metrics
Emit query latency metrics
2 parents 045c1fe + 25def94 commit 8015c79

4 files changed

Lines changed: 72 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
@@ -130,6 +130,18 @@ func EmitThrottleInterval(emit Emitter, duration time.Duration, reason string) {
130130
emit.Count("throttle.events_total", 1, tags...)
131131
}
132132

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+
}
144+
133145
// RecordSleep emits per-stage sleep/wait metrics (namespace is applied by the client):
134146
// gh_ost.sleep.duration_milliseconds and gh_ost.sleep.total_milliseconds, both tagged by stage.
135147
func RecordSleep(emit Emitter, stage string, d time.Duration) {

go/metrics/emit_test.go

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

88
import (
99
"context"
10+
"errors"
1011
"runtime"
1112
"slices"
1213
"testing"
@@ -258,6 +259,49 @@ func TestEmitThrottleIntervalNilSafe(t *testing.T) {
258259
EmitThrottleInterval(&gaugeSpy{}, time.Second, "test")
259260
}
260261

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+
}
304+
261305
type sleepSpy struct {
262306
histogramNames []string
263307
histogramValues []float64

0 commit comments

Comments
 (0)