blob: 78ab72e7955ba6e1131731de4f372869f1e219fa [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package datasources
import (
"context"
"math/big"
"sort"
"cloud.google.com/go/bigquery"
"cloud.google.com/go/civil"
"google.golang.org/api/iterator"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/grpc/appstatus"
"infra/appengine/statsui/api"
)
// Client is used to fetch metrics from a given data source.
type Client struct {
Client *bigquery.Client
Config *Config
}
func bqToDateArray(dates []string) ([]civil.Date, error) {
ret := make([]civil.Date, len(dates))
for i, date := range dates {
d, err := civil.ParseDate(date)
if err != nil {
return nil, err
}
ret[i] = d
}
return ret, nil
}
func (c *Client) GetMetrics(ctx context.Context, dataSource string, period api.Period, dates, metrics []string) ([]*api.Section, error) {
if _, exists := c.Config.Sources[dataSource]; !exists {
return nil, appstatus.Errorf(codes.InvalidArgument, "data source %q not found", dataSource)
}
if _, exists := c.Config.Sources[dataSource].Queries[period.String()]; !exists {
return nil, appstatus.Errorf(codes.InvalidArgument, "period %q not available for data source %q", period, dataSource)
}
type LabelValue struct {
Label string
Value *big.Rat
}
type Row struct {
Date civil.Date
Section string
Metric string
Value *big.Rat
Aggregate []LabelValue
}
q := c.Client.Query(c.Config.Sources[dataSource].Queries[period.String()])
bqDates, err := bqToDateArray(dates)
if err != nil {
return nil, err
}
q.Parameters = []bigquery.QueryParameter{
{Name: "dates", Value: bqDates},
{Name: "metrics", Value: metrics},
}
it, err := q.Read(ctx)
if err != nil {
return nil, err
}
data := make(map[string]map[string]*api.Metric)
for {
var row Row
err := it.Next(&row)
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
section, ok := data[row.Section]
if !ok {
section = make(map[string]*api.Metric)
data[row.Section] = section
}
metric, ok := section[row.Metric]
if !ok {
metric = &api.Metric{
Name: row.Metric,
}
section[metric.Name] = metric
}
if row.Aggregate != nil {
for _, val := range row.Aggregate {
dataSet, ok := metric.Sections[val.Label]
if !ok {
dataSet = &api.DataSet{Data: make(map[string]float32)}
if metric.Sections == nil {
metric.Sections = make(map[string]*api.DataSet)
}
metric.Sections[val.Label] = dataSet
}
dataSet.Data[row.Date.String()], _ = val.Value.Float32()
}
} else if row.Value != nil {
if metric.Data == nil {
metric.Data = &api.DataSet{Data: make(map[string]float32)}
}
metric.Data.Data[row.Date.String()], _ = row.Value.Float32()
}
}
sections := make([]*api.Section, 0, len(data))
for sectionName, m := range data {
section := &api.Section{
Name: sectionName,
Metrics: make([]*api.Metric, 0, len(m)),
}
for _, metric := range m {
section.Metrics = append(section.Metrics, metric)
}
sort.Slice(section.Metrics, func(i, j int) bool {
return section.Metrics[i].Name < section.Metrics[j].Name
})
sections = append(sections, section)
}
sort.Slice(sections, func(i, j int) bool {
return sections[i].Name < sections[j].Name
})
return sections, nil
}