Skip to content

Commit

Permalink
Metrics: Contract set churn (#675)
Browse files Browse the repository at this point in the history
* stores: implement contract set churn metrics

* stores: fix build
  • Loading branch information
ChrisSchinnerl authored Oct 19, 2023
1 parent 2b9268d commit f0805a4
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 5 deletions.
23 changes: 22 additions & 1 deletion api/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,16 +565,37 @@ func ObjectPathEscape(path string) string {
return url.PathEscape(strings.TrimPrefix(path, "/"))
}

const (
ChurnDirAdded = "added"
ChurnDirRemoved = "removed"
)

type (
ContractSetMetric struct {
Contracts int `json:"contracts"`
Name string `json:"name"`
Time time.Time `json:"time"`
Timestamp time.Time `json:"timestamp"`
}

ContractSetMetricsQueryOpts struct {
Name string
After time.Time
Before time.Time
}

ContractSetChurnMetric struct {
Direction string `json:"direction"`
FCID types.FileContractID `json:"fcid"`
Name string `json:"name"`
Reason string `json:"reason"`
Timestamp time.Time `json:"timestamp"`
}

ContractSetChurnMetricsQueryOpts struct {
Name string
After time.Time
Before time.Time
Direction string
Reason string
}
)
57 changes: 55 additions & 2 deletions stores/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"time"

"go.sia.tech/core/types"
"go.sia.tech/renterd/api"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -94,7 +95,7 @@ func scopeTimeRange(tx *gorm.DB, after, before time.Time) *gorm.DB {
func (s *SQLStore) contractSetMetrics(ctx context.Context, opts api.ContractSetMetricsQueryOpts) ([]dbContractSetMetric, error) {
tx := s.dbMetrics
if opts.Name != "" {
tx = tx.Where("name = ?", opts.Name)
tx = tx.Where("name", opts.Name)
}
var metrics []dbContractSetMetric
err := tx.Scopes(func(tx *gorm.DB) *gorm.DB {
Expand All @@ -119,7 +120,7 @@ func (s *SQLStore) ContractSetMetrics(ctx context.Context, opts api.ContractSetM
resp[i] = api.ContractSetMetric{
Contracts: metrics[i].Contracts,
Name: metrics[i].Name,
Time: time.Time(metrics[i].Timestamp).UTC(),
Timestamp: time.Time(metrics[i].Timestamp).UTC(),
}
}
return resp, nil
Expand All @@ -132,3 +133,55 @@ func (s *SQLStore) RecordContractSetMetric(ctx context.Context, t time.Time, set
Timestamp: unixTimeMS(t),
}).Error
}

func (s *SQLStore) contractSetChurnMetrics(ctx context.Context, opts api.ContractSetChurnMetricsQueryOpts) ([]dbContractSetChurnMetric, error) {
tx := s.dbMetrics
if opts.Name != "" {
tx = tx.Where("name", opts.Name)
}
if opts.Direction != "" {
tx = tx.Where("direction", opts.Direction)
}
if opts.Reason != "" {
tx = tx.Where("reason", opts.Reason)
}
var metrics []dbContractSetChurnMetric
err := tx.Scopes(func(tx *gorm.DB) *gorm.DB {
return scopeTimeRange(tx, opts.After, opts.Before)
}).
Order("timestamp ASC").
Find(&metrics).
Error
if err != nil {
return nil, fmt.Errorf("failed to fetch contract set metrics: %w", err)
}
return metrics, nil
}

func (s *SQLStore) ContractSetChurnMetrics(ctx context.Context, opts api.ContractSetChurnMetricsQueryOpts) ([]api.ContractSetChurnMetric, error) {
metrics, err := s.contractSetChurnMetrics(ctx, opts)
if err != nil {
return nil, err
}
resp := make([]api.ContractSetChurnMetric, len(metrics))
for i := range resp {
resp[i] = api.ContractSetChurnMetric{
Direction: metrics[i].Direction,
FCID: types.FileContractID(metrics[i].FCID),
Name: metrics[i].Name,
Reason: metrics[i].Reason,
Timestamp: time.Time(metrics[i].Timestamp).UTC(),
}
}
return resp, nil
}

func (s *SQLStore) RecordContractSetChurnMetric(ctx context.Context, t time.Time, set, direction, reason string, fcid types.FileContractID) error {
return s.dbMetrics.Create(&dbContractSetChurnMetric{
Direction: string(direction),
FCID: fileContractID(fcid),
Name: set,
Reason: reason,
Timestamp: unixTimeMS(t),
}).Error
}
105 changes: 103 additions & 2 deletions stores/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"go.sia.tech/core/types"
"go.sia.tech/renterd/api"
)

Expand All @@ -21,7 +22,7 @@ func TestContractSetMetrics(t *testing.T) {
t.Fatal(err)
} else if m := metrics[0]; m.Contracts != 0 {
t.Fatalf("expected 0 contracts, got %v", m.Contracts)
} else if !time.Time(m.Time).After(testStart) {
} else if !time.Time(m.Timestamp).After(testStart) {
t.Fatal("expected time to be after test start")
} else if m.Name != testContractSet {
t.Fatalf("expected name to be %v, got %v", testContractSet, m.Name)
Expand All @@ -47,7 +48,7 @@ func TestContractSetMetrics(t *testing.T) {
if len(metrics) != expected {
t.Fatalf("expected %v metrics, got %v", expected, len(metrics))
} else if !sort.SliceIsSorted(metrics, func(i, j int) bool {
return time.Time(metrics[i].Time).Before(time.Time(metrics[j].Time))
return time.Time(metrics[i].Timestamp).Before(time.Time(metrics[j].Timestamp))
}) {
t.Fatal("expected metrics to be sorted by time")
}
Expand All @@ -69,3 +70,103 @@ func TestContractSetMetrics(t *testing.T) {
before := time.UnixMilli(2) // 'before' is inclusive
assertMetrics(api.ContractSetMetricsQueryOpts{After: after, Before: before}, 1, []int{3})
}

func TestContractChurnSetMetrics(t *testing.T) {
ss := newTestSQLStore(t, defaultTestSQLStoreConfig)
defer ss.Close()

// Create metrics to query.
sets := []string{"foo", "bar"}
directions := []string{api.ChurnDirAdded, api.ChurnDirRemoved}
reasons := []string{"reasonA", "reasonB"}
times := []time.Time{time.UnixMilli(3), time.UnixMilli(1), time.UnixMilli(2)}
var i byte
for _, set := range sets {
for _, dir := range directions {
for _, reason := range reasons {
for _, recordedTime := range times {
fcid := types.FileContractID{i}
if err := ss.RecordContractSetChurnMetric(context.Background(), recordedTime, set, dir, reason, fcid); err != nil {
t.Fatal(err)
}
i++
}
}
}
}

// Query without any filters.
metrics, err := ss.contractSetChurnMetrics(context.Background(), api.ContractSetChurnMetricsQueryOpts{})
if err != nil {
t.Fatal(err)
} else if len(metrics) != 24 {
t.Fatalf("expected 24 metrics, got %v", len(metrics))
} else if !sort.SliceIsSorted(metrics, func(i, j int) bool {
return time.Time(metrics[i].Timestamp).Before(time.Time(metrics[j].Timestamp))
}) {
t.Fatal("expected metrics to be sorted by time")
}

// Query by set name.
metrics, err = ss.contractSetChurnMetrics(context.Background(), api.ContractSetChurnMetricsQueryOpts{
Name: sets[0],
})
if err != nil {
t.Fatal(err)
} else if len(metrics) != 12 {
t.Fatalf("expected 12 metrics, got %v", len(metrics))
}
for _, m := range metrics {
if m.Name != sets[0] {
t.Fatalf("expected name to be %v, got %v", sets[0], m.Name)
}
}

// Query by time.
after := time.UnixMilli(2) // 'after' is exclusive
before := time.UnixMilli(3) // 'before' is inclusive
metrics, err = ss.contractSetChurnMetrics(context.Background(), api.ContractSetChurnMetricsQueryOpts{
After: after,
Before: before,
})
if err != nil {
t.Fatal(err)
} else if len(metrics) != 8 {
t.Fatalf("expected 8 metrics, got %v", len(metrics))
}
for _, m := range metrics {
if m.Timestamp != unixTimeMS(before) {
t.Fatalf("expected time to be %v, got %v", before, time.Time(m.Timestamp).UnixMilli())
}
}

// Query by direction.
metrics, err = ss.contractSetChurnMetrics(context.Background(), api.ContractSetChurnMetricsQueryOpts{
Direction: directions[1],
})
if err != nil {
t.Fatal(err)
} else if len(metrics) != 12 {
t.Fatalf("expected 12 metrics, got %v", len(metrics))
}
for _, m := range metrics {
if m.Direction != directions[1] {
t.Fatalf("expected direction to be %v, got %v", directions[1], m.Direction)
}
}

// Query by reason.
metrics, err = ss.contractSetChurnMetrics(context.Background(), api.ContractSetChurnMetricsQueryOpts{
Reason: reasons[1],
})
if err != nil {
t.Fatal(err)
} else if len(metrics) != 12 {
t.Fatalf("expected 12 metrics, got %v", len(metrics))
}
for _, m := range metrics {
if m.Reason != reasons[1] {
t.Fatalf("expected reason to be %v, got %v", reasons[1], m.Reason)
}
}
}

0 comments on commit f0805a4

Please sign in to comment.