Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EVG-5410 Add functions to query execution statistics #1744

Merged
merged 9 commits into from Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
818 changes: 661 additions & 157 deletions model/stats/db.go

Large diffs are not rendered by default.

265 changes: 265 additions & 0 deletions model/stats/query.go
@@ -0,0 +1,265 @@
package stats

import (
"time"

"github.com/evergreen-ci/evergreen/db"
"github.com/evergreen-ci/evergreen/util"
"github.com/mongodb/grip"
"github.com/pkg/errors"
)

const (
MaxQueryLimit = 1000
GroupByTest GroupBy = "test"
GroupByTask GroupBy = "task"
GroupByVariant GroupBy = "variant"
GroupByDistro GroupBy = "distro"
SortEarliestFirst Sort = "earliest"
SortLatestFirst Sort = "latest"
)

type GroupBy string

func (gb GroupBy) validate() error {
switch gb {
case GroupByDistro:
case GroupByVariant:
case GroupByTask:
case GroupByTest:
default:
return errors.Errorf("Invalid GroupBy value: %s", gb)
}
return nil
}

type Sort string

func (s Sort) validate() error {
switch s {
case SortLatestFirst:
case SortEarliestFirst:
default:
return errors.Errorf("Invalid Sort value: %s", s)
}
return nil
}

/////////////
// Filters //
/////////////

// StartAt represents parameters that allow a search query to resume at a specific point.
// Used for pagination.
type StartAt struct {
Date time.Time
BuildVariant string
Task string
Test string
Distro string
}

// validateCommon validates that the StartAt struct is valid for use with test stats.
func (s *StartAt) validateCommon(groupBy GroupBy) error {
catcher := grip.NewBasicCatcher()
if s == nil {
catcher.Add(errors.New("StartAt should not be nil"))
}
if !s.Date.Equal(util.GetUTCDay(s.Date)) {
catcher.Add(errors.New("Invalid StartAt Date value"))
}
switch groupBy {
case GroupByDistro:
if len(s.Distro) == 0 {
catcher.Add(errors.New("Missing StartAt Distro value"))
}
fallthrough
tychoish marked this conversation as resolved.
Show resolved Hide resolved
case GroupByVariant:
if len(s.BuildVariant) == 0 {
catcher.Add(errors.New("Missing StartAt BuildVariant value"))
}
fallthrough
case GroupByTask:
if len(s.Task) == 0 {
catcher.Add(errors.New("Missing StartAt Task value"))
}
}
return catcher.Resolve()
}

// validateForTests validates that the StartAt struct is valid for use with test stats.
func (s *StartAt) validateForTests(groupBy GroupBy) error {
catcher := grip.NewBasicCatcher()
catcher.Add(s.validateCommon(groupBy))
if len(s.Test) == 0 {
catcher.Add(errors.New("Missing Start Test value"))
}
return catcher.Resolve()
}

// validateForTasks validates that the StartAt struct is valid for use with task stats.
func (s *StartAt) validateForTasks(groupBy GroupBy) error {
catcher := grip.NewBasicCatcher()
catcher.Add(s.validateCommon(groupBy))
if len(s.Test) != 0 {
catcher.Add(errors.New("StartAt for task stats should not have a Test value"))
}
return catcher.Resolve()
}

// StatsFilter represents search and aggregation parameters when querying the test or task statistics.
type StatsFilter struct {
Project string
Requesters []string
AfterDate time.Time
BeforeDate time.Time

Tests []string
Tasks []string
BuildVariants []string
Distros []string

GroupNumDays int
GroupBy GroupBy
StartAt *StartAt
Limit int
Sort Sort
}

// validateCommon performs common validations regardless of the filter's intended use.
func (f *StatsFilter) validateCommon() error {
catcher := grip.NewBasicCatcher()
if f == nil {
catcher.Add(errors.New("StatsFilter should not be nil"))
}

if f.GroupNumDays <= 0 {
catcher.Add(errors.New("Invalid GroupNumDays value"))
}
if !f.AfterDate.Equal(util.GetUTCDay(f.AfterDate)) {
catcher.Add(errors.New("Invalid AfterDate value"))
}
if !f.BeforeDate.Equal(util.GetUTCDay(f.BeforeDate)) {
catcher.Add(errors.New("Invalid BeforeDate value"))
}
if !f.BeforeDate.After(f.AfterDate) {
catcher.Add(errors.New("Invalid AfterDate/BeforeDate values"))
}
if len(f.Requesters) == 0 {
catcher.Add(errors.New("Missing Requesters values"))
}
if f.Limit > MaxQueryLimit || f.Limit <= 0 {
catcher.Add(errors.New("Invalid Limit value"))
}
catcher.Add(f.Sort.validate())
catcher.Add(f.GroupBy.validate())

return catcher.Resolve()
}

// validateForTests validates that the StatsFilter struct is valid for use with test stats.
func (f *StatsFilter) validateForTests() error {
catcher := grip.NewBasicCatcher()

catcher.Add(f.validateCommon())
if f.StartAt != nil {
catcher.Add(f.StartAt.validateForTests(f.GroupBy))
}
if len(f.Tests) == 0 && len(f.Tasks) == 0 {
catcher.Add(errors.New("Missing Tests or Tasks values"))
}

return catcher.Resolve()
}

//use with test stats validates that the StatsFilter struct is valid for use with task stats.
func (f *StatsFilter) validateForTasks() error {
catcher := grip.NewBasicCatcher()

catcher.Add(f.validateCommon())
if f.StartAt != nil {
catcher.Add(f.StartAt.validateForTasks(f.GroupBy))
}
if len(f.Tests) > 0 {
catcher.Add(errors.New("Invalid Tests value, should be nil or empty"))
}
if len(f.Tasks) == 0 {
catcher.Add(errors.New("Missing Tasks values"))
}
if f.GroupBy == GroupByTest {
catcher.Add(errors.New("Invalid GroupBy value for a task filter"))
}

return catcher.Resolve()

}

//////////////////////////////
// Test Statistics Querying //
//////////////////////////////

// TestStats represents test execution statistics.
type TestStats struct {
TestFile string `bson:"test_file"`
TaskName string `bson:"task_name"`
BuildVariant string `bson:"variant"`
Distro string `bson:"distro"`
Date time.Time `bson:"date"`

NumPass int `bson:"num_pass"`
NumFail int `bson:"num_fail"`
AvgDurationPass float64 `bson:"avg_duration_pass"`
LastUpdate time.Time `bson:"last_update"`
}

// GetTestStats queries the precomputed test statistics using a filter.
func GetTestStats(filter *StatsFilter) ([]TestStats, error) {
err := filter.validateForTests()
if err != nil {
return nil, errors.Wrap(err, "The provided StatsFilter is invalid")
}
var stats []TestStats
pipeline := testStatsQueryPipeline(filter)
err = db.Aggregate(dailyTestStatsCollection, pipeline, &stats)
if err != nil {
return nil, errors.Wrap(err, "Failed to aggregate test statistics")
}
return stats, nil
}

//////////////////////////////
// Task Statistics Querying //
//////////////////////////////

// TaskStats represents task execution statistics.
type TaskStats struct {
TaskName string `bson:"task_name"`
BuildVariant string `bson:"variant"`
Distro string `bson:"distro"`
Date time.Time `bson:"date"`

NumTotal int `bson:"num_total"`
NumSuccess int `bson:"num_success"`
NumFailed int `bson:"num_failed"`
NumTimeout int `bson:"num_timeout"`
NumTestFailed int `bson:"num_test_failed"`
NumSystemFailed int `bson:"num_system_failed"`
NumSetupFailed int `bson:"num_setup_failed"`
AvgDurationSuccess float64 `bson:"avg_duration_success"`
LastUpdate time.Time `bson:"last_update"`
tychoish marked this conversation as resolved.
Show resolved Hide resolved
tychoish marked this conversation as resolved.
Show resolved Hide resolved
}

// GetTaskStats queries the precomputed task statistics using a filter.
func GetTaskStats(filter *StatsFilter) ([]TaskStats, error) {
err := filter.validateForTasks()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nil check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the validation method.

if err != nil {
return nil, errors.Wrap(err, "The provided StatsFilter is invalid")
}
var stats []TaskStats
pipeline := taskStatsQueryPipeline(filter)
err = db.Aggregate(dailyTaskStatsCollection, pipeline, &stats)
if err != nil {
return nil, errors.Wrap(err, "Failed to aggregate task statistics")
}
return stats, nil
}