Skip to content

Commit

Permalink
[probestatus] Add graphs to the probestatus page \o/. (cloudprober#132)
Browse files Browse the repository at this point in the history
= Very excited about this change. Probestatus page will now show success
ratio graphs. Note that for now probestatus page is enabled by adding the
probestatus surfacer: `surfacer {type: PROBESTATUS}`.

= There will be one graph per probe, and graph lines will correspond to
the targets. See cloudprober#132 for an example screenshot.

= We use timeseries compression (values and frequencies) for the graph data to
minimize memory allocation in Cloudprober at the time of graph building. This also
helps the probestatus page size not depend on the data size (number of values).

= Graphs also support the following URL params: graph_endpoint and
graph_duration to move around in the graph. You can set these params only in the
URL right now, I'll add HTML inputs in a later change.

= We are currently using C3 JS for graphing, and shipping all the required JS in
Cloudprober itself. We'll re-evaluate both the decisions in future.
  • Loading branch information
manugarg authored Apr 8, 2022
1 parent d62ad43 commit 7f3a0f3
Show file tree
Hide file tree
Showing 13 changed files with 679 additions and 44 deletions.
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ require (
google.golang.org/protobuf v1.27.0
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

require (
github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 // indirect
Expand All @@ -35,6 +41,7 @@ require (
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/stretchr/testify v1.7.1
go.opencensus.io v0.23.0 // indirect
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -524,6 +526,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
193 changes: 193 additions & 0 deletions surfacers/probestatus/graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright 2022 The Cloudprober Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package probestatus

import (
"encoding/json"
"time"

"github.com/cloudprober/cloudprober/sysvars"
)

func graphResolution(endTime time.Time, td time.Duration) time.Duration {
dataTime := endTime.Sub(sysvars.StartTime())
if dataTime < td {
td = dataTime
}

if td <= 6*time.Hour {
return time.Minute
}
if td <= 24*time.Hour {
return 5 * time.Minute
}
return 15 * time.Minute
}

func (gd *graphData) syncGraphLines(startTime, endTime map[string]time.Time) {
var minStartTime, maxEndTime time.Time
var needLeftPadding, needRightPadding bool

for _, t := range startTime {
if minStartTime.IsZero() {
minStartTime = t
continue
}
if t != minStartTime {
if t.Before(minStartTime) {
minStartTime = t
}
needLeftPadding = true
}
}
for _, t := range endTime {
if maxEndTime.IsZero() {
maxEndTime = t
continue
}
if t != maxEndTime {
if t.After(maxEndTime) {
maxEndTime = t
}
needRightPadding = true
}
}

if needLeftPadding {
for tgt, t := range startTime {
if t.After(minStartTime) {
lpadding := int(t.Sub(minStartTime).Seconds()) / gd.ResSeconds
gd.Values[tgt] = append([]float64{-1}, gd.Values[tgt]...)
gd.Freqs[tgt] = append([]int64{int64(lpadding)}, gd.Freqs[tgt]...)
}
}
}
if needRightPadding {
for tgt, t := range endTime {
if t.Before(maxEndTime) {
rpadding := int(maxEndTime.Sub(t).Seconds()) / gd.ResSeconds
gd.Values[tgt] = append(gd.Values[tgt], -1)
gd.Freqs[tgt] = append(gd.Freqs[tgt], int64(rpadding))
}
}
}
gd.StartTime = minStartTime.Unix()
gd.EndTime = maxEndTime.Unix()
}

type graphPoints struct {
startTime, endTime time.Time
values []float64
freqs []int64
}

func computeGraphPoints(baseTS *timeseries, endTime time.Time, td, res time.Duration) *graphPoints {
// Return nothing if duration is too small.
if td < baseTS.res {
return nil
}

ts := baseTS.shallowCopy()

gp := &graphPoints{endTime: endTime}

// Truncate latest if endTime is before the current timeseries time.
ts.l.Debugf("timeseries before any change: ts.oldest=%d, ts.latest=%d", ts.oldest, ts.latest)
if endTime.Before(ts.currentTS) {
ts.latest = ts.agoIndex(int(ts.currentTS.Sub(endTime) / ts.res))
} else {
gp.endTime = ts.currentTS
}
ts.l.Debugf("timeseries after endTime truncated: ts.oldest=%d, ts.latest=%d", ts.oldest, ts.latest)

if ts.latest == ts.oldest {
return nil
}

// Let's move oldest now if needed.
if int(td/ts.res) < ts.size() {
ts.oldest = ts.agoIndex(int(td / ts.res))
gp.startTime = gp.endTime.Add(-td)
} else {
gp.startTime = gp.endTime.Add(-time.Duration(ts.size()) * ts.res)
}

ts.l.Debugf("timeseries after startTime truncated: ts.oldest=%d, ts.latest=%d", ts.oldest, ts.latest)

step := int(res / ts.res)
if step == 0 {
step = 1
}
// Note gd.ResCount will be 1, until we've a bunch of points.
numValues := ts.size() / step
var lastVal float64
for i := numValues - 1; i >= 0; i = i - 1 {
// Using agoIndex makes sure tts.latest won't jump beyond tts.oldest.
if ts.latest == ts.oldest {
break
}

currentD := ts.a[ts.latest]
ts.latest = ts.agoIndex(step)
lastD := ts.a[ts.latest]

val := float64(currentD.success-lastD.success) / float64(currentD.total-lastD.total)
if val == lastVal && len(gp.freqs) > 0 {
gp.freqs[0]++
} else {
gp.values = append([]float64{val}, gp.values...)
gp.freqs = append([]int64{1}, gp.freqs...)
lastVal = val
}
}

return gp
}

type graphData struct {
StartTime, EndTime int64
ResSeconds int // Used by HTML template
Values map[string][]float64
Freqs map[string][]int64
}

func (gd *graphData) JSONBytes() []byte {
gdJSON, _ := json.Marshal(gd)
return gdJSON
}

func computeGraphData(metrics map[string]*timeseries, endTime time.Time, td time.Duration) *graphData {
gd := &graphData{
Values: make(map[string][]float64),
Freqs: make(map[string][]int64),
ResSeconds: int(graphResolution(endTime, td).Seconds()),
}

startTimes, endTimes := make(map[string]time.Time), make(map[string]time.Time)

for targetName, ts := range metrics {
gp := computeGraphPoints(ts, endTime, td, time.Duration(gd.ResSeconds)*time.Second)
if gp == nil {
continue
}
startTimes[targetName], endTimes[targetName] = gp.startTime, gp.endTime
gd.Values[targetName], gd.Freqs[targetName] = gp.values, gp.freqs
}

gd.syncGraphLines(startTimes, endTimes)

//ps.l.Debugf("graphData[%s]: %s", probeName, string(gd.JSONBytes()))
return gd
}
Loading

0 comments on commit 7f3a0f3

Please sign in to comment.