blob: 3063c49ab3dbf0bf30ac659ee5eaffcc040c05c9 [file] [log] [blame]
// Copyright 2016 The Vanadium 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 main
import (
"encoding/json"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
monitoring "google.golang.org/api/monitoring/v3"
"v.io/x/lib/gcm"
)
const (
defaultDuration = time.Hour
gcmAuthTimeout = time.Hour
)
var (
dashboardIndexCache = []byte{}
gcmService *monitoring.Service
gcmLastAuthTime time.Time
)
type point struct {
Timestamp int64
Value float64
}
type points []point
func (pts points) Len() int { return len(pts) }
func (pts points) Less(i, j int) bool { return pts[i].Timestamp < pts[j].Timestamp }
func (pts points) Swap(i, j int) { pts[i], pts[j] = pts[j], pts[i] }
func (pts points) Sort() { sort.Sort(pts) }
type statsResult struct {
SysMemUsageBytes points
SysMemUsagePct points
SysDiskUsageBytes points
SysDiskUsagePct points
SysCPUUsagePct points
Qps points
Latency points
MinTime int64
MaxTime int64
NotLoggedIn bool
}
func handleDashboard(ss *serverState, rs *requestState) error {
ctx := ss.ctx
instance := rs.r.FormValue(paramInstance)
if instance == "" {
return fmt.Errorf("parameter %q required for instance name", paramInstance)
}
if err := checkOwnerOfInstance(ctx, rs.email, instance); err != nil {
return err
}
tmplArgs := struct {
ServerName,
Instance,
Email string
}{
ServerName: ss.args.serverName,
Instance: instance,
Email: rs.email,
}
if err := ss.args.assets.executeTemplate(rs.w, dashboardTmpl, tmplArgs); err != nil {
return fmt.Errorf("failed to render dashboard template: %v", err)
}
return nil
}
// TODO(jingjin): Returning an error from handleStats will cause an error page
// to be rendered, which is not what we want when we consume the HTTP response
// via Ajax.
// handleStats responds to /stats request. It retrieves time series data
// for the given syncbase instance from GCM.
func handleStats(ss *serverState, rs *requestState) error {
ctx := ss.ctx
var result statsResult
writeResult := func() error {
// Convert result to json and return it.
b, err := json.MarshalIndent(&result, "", " ")
if err != nil {
return err
}
rs.w.Header().Set("Content-Type", "application/json")
rs.w.Write(b)
return nil
}
if rs.email == "" {
result.NotLoggedIn = true
return writeResult()
}
instance := rs.r.FormValue(paramInstance)
if instance == "" {
return fmt.Errorf("parameter %q required for instance name", paramInstance)
}
if err := checkOwnerOfInstance(ctx, rs.email, instance); err != nil {
return err
}
now := time.Now()
if gcmService == nil || now.Sub(gcmLastAuthTime) > gcmAuthTimeout {
s, err := gcm.Authenticate(ss.args.monitoringKeyFile)
if err != nil {
return err
}
gcmService = s
gcmLastAuthTime = now
}
// Get duration (default to 1h) and instance mounted name.
duration := defaultDuration
if strDuration := rs.r.FormValue(paramDashbordDuration); strDuration != "" {
d, err := strconv.ParseInt(strDuration, 10, 64)
if err != nil {
return err
}
duration = time.Duration(d) * time.Second
}
// Get data from GCM.
md, err := gcm.GetMetric(ss.args.dashboardGCMMetric, ss.args.dashboardGCMProject)
if err != nil {
return err
}
filters := []string{
fmt.Sprintf("metric.type=%q", md.Type),
// TODO(jingjin): Can we make the key be just the instance name
// (without sb/ prefix)?
fmt.Sprintf("metric.label.mounted_name=%q", relativeMountName(instance)),
}
nextPageToken := ""
tsMap := map[string]points{}
for {
listCall := gcmService.Projects.TimeSeries.List(fmt.Sprintf("projects/%s", ss.args.dashboardGCMProject)).
IntervalStartTime(now.Add(-duration).UTC().Format(time.RFC3339)).
IntervalEndTime(now.UTC().Format(time.RFC3339)).
Filter(strings.Join(filters, " AND ")).
PageToken(nextPageToken)
alignmentPeriod := getAlignmentPeriodInSeconds(duration)
if alignmentPeriod >= 0 {
listCall = listCall.AggregationAlignmentPeriod(fmt.Sprintf("%ds", alignmentPeriod)).AggregationPerSeriesAligner("ALIGN_MEAN")
}
resp, err := listCall.Do()
if err != nil {
return err
}
for _, ts := range resp.TimeSeries {
metricName := ts.Metric.Labels["metric_name"]
for _, pt := range ts.Points {
epochTime, err := time.Parse(time.RFC3339, pt.Interval.EndTime)
if err != nil {
ctx.Errorf("Parse(%s) failed: %v", pt.Interval.EndTime, err)
continue
}
tsMap[metricName] = append(tsMap[metricName], point{
Timestamp: epochTime.Unix(),
Value: pt.Value.DoubleValue,
})
}
}
nextPageToken = resp.NextPageToken
if nextPageToken == "" {
break
}
}
// Process data and put it into statsResult.
minTime := int64(math.MaxInt64)
maxTime := int64(0)
for metricName, pts := range tsMap {
tsMap[metricName].Sort()
if pts[0].Timestamp < minTime {
minTime = pts[0].Timestamp
}
if pts[len(pts)-1].Timestamp > maxTime {
maxTime = pts[len(pts)-1].Timestamp
}
switch metricName {
case "sysmem-usage-bytes":
result.SysMemUsageBytes = pts
case "sysmem-usage-pct":
result.SysMemUsagePct = pts
case "sysdisk-usage-bytes":
result.SysDiskUsageBytes = pts
case "sysdisk-usage-pct":
result.SysDiskUsagePct = pts
case "syscpu-usage-pct":
result.SysCPUUsagePct = pts
case "latency":
result.Latency = pts
case "qps":
result.Qps = pts
}
}
result.MinTime = minTime
result.MaxTime = maxTime
return writeResult()
}
func getAlignmentPeriodInSeconds(duration time.Duration) int {
switch {
case duration <= time.Hour*12:
return -1
case duration <= time.Hour*24:
return 5 * 60
case duration <= time.Hour*24*7:
return 20 * 60
case duration <= time.Hour*24*30:
return 30 * 60
default:
return 60 * 60
}
}