mirror of
https://github.com/Luzifer/mondash.git
synced 2024-11-10 00:20:02 +00:00
Knut Ahlers
600f2f8448
Instead of just showing three bars with different colors which is quite useless overall now segments of the progress bar are generated according to the state of the check at the corresponding point of time. The effect is the viewer can see how recently the status was triggered instead just seeing a percentage. Signed-off-by: Knut Ahlers <knut@ahlers.me>
327 lines
7.4 KiB
Go
327 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/Luzifer/mondash/storage"
|
|
)
|
|
|
|
type dashboard struct {
|
|
DashboardID string `json:"-"`
|
|
APIKey string `json:"api_key"`
|
|
Metrics dashboardMetrics `json:"metrics"`
|
|
storage storage.Storage
|
|
}
|
|
|
|
func loadDashboard(dashid string, store storage.Storage) (*dashboard, error) {
|
|
data, err := store.Get(dashid)
|
|
if err != nil {
|
|
return &dashboard{}, errors.New("Dashboard not found")
|
|
}
|
|
|
|
tmp := &dashboard{
|
|
DashboardID: dashid,
|
|
storage: store,
|
|
}
|
|
_ = json.Unmarshal(data, tmp)
|
|
|
|
return tmp, nil
|
|
}
|
|
|
|
func (d *dashboard) Save() {
|
|
data, err := json.Marshal(d)
|
|
if err != nil {
|
|
log.Printf("Error while marshalling dashboard: %s", err)
|
|
return
|
|
}
|
|
err = d.storage.Put(d.DashboardID, data)
|
|
if err != nil {
|
|
log.Printf("Error while storing dashboard: %s", err)
|
|
}
|
|
}
|
|
|
|
type dashboardMetrics []*dashboardMetric
|
|
|
|
func (a dashboardMetrics) Len() int { return len(a) }
|
|
func (a dashboardMetrics) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a dashboardMetrics) Less(i, j int) bool {
|
|
return a[i].Meta.LastUpdate.Before(a[j].Meta.LastUpdate)
|
|
}
|
|
|
|
type dashboardMetric struct {
|
|
MetricID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
Value float64 `json:"value,omitifempty"`
|
|
Expires int64 `json:"expires,omitifempty"`
|
|
Freshness int64 `json:"freshness,omitifempty"`
|
|
IgnoreMAD bool `json:"ignore_mad"`
|
|
HideMAD bool `json:"hide_mad"`
|
|
HideValue bool `json:"hide_value"`
|
|
HistoricalData dashboardMetricHistory `json:"history,omitifempty"`
|
|
Meta dashboardMetricMeta `json:"meta,omitifempty"`
|
|
}
|
|
|
|
type dashboardMetricStatus struct {
|
|
Time time.Time `json:"time"`
|
|
Status string `json:"status"`
|
|
Value float64 `json:"value"`
|
|
}
|
|
|
|
type dashboardMetricMeta struct {
|
|
LastUpdate time.Time
|
|
LastOK time.Time
|
|
PercOK float64
|
|
PercWarn float64
|
|
PercCrit float64
|
|
}
|
|
|
|
type dashboardMetricHistory []dashboardMetricStatus
|
|
|
|
func newDashboardMetric() *dashboardMetric {
|
|
return &dashboardMetric{
|
|
Status: "Unknown",
|
|
Expires: 604800,
|
|
Freshness: 3600,
|
|
HistoricalData: dashboardMetricHistory{},
|
|
Meta: dashboardMetricMeta{},
|
|
}
|
|
}
|
|
|
|
func median(values []float64) float64 {
|
|
sort.Float64s(values)
|
|
|
|
if len(values) == 1 {
|
|
return values[0]
|
|
}
|
|
|
|
// If even, take an average
|
|
if len(values)%2 == 0 {
|
|
return 0.5*values[len(values)/2] + 0.5*values[len(values)/2-1]
|
|
}
|
|
|
|
log.Printf("len(values)=%v, len(values)/2=%v\n", len(values), len(values)/2)
|
|
|
|
return values[len(values)/2-1]
|
|
}
|
|
|
|
func absoluteValue(value float64) float64 {
|
|
if value < 0 {
|
|
value = -value
|
|
}
|
|
return value
|
|
}
|
|
|
|
func absoluteDeviation(values []float64) []float64 {
|
|
medianValue := median(values)
|
|
|
|
deviation := make([]float64, len(values))
|
|
|
|
for i := range values {
|
|
deviation[i] = absoluteValue(values[i] - medianValue)
|
|
}
|
|
|
|
return deviation
|
|
}
|
|
|
|
func (dm *dashboardMetric) getValueArray() []float64 {
|
|
values := []float64{}
|
|
|
|
for _, v := range dm.HistoricalData {
|
|
values = append(values, v.Value)
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
func (dm *dashboardMetric) Median() float64 {
|
|
return median(dm.getValueArray())
|
|
}
|
|
|
|
func (dm *dashboardMetric) MedianAbsoluteDeviation() (float64, float64) {
|
|
values := dm.getValueArray()
|
|
medianValue := dm.Median()
|
|
|
|
return medianValue, median(absoluteDeviation(values))
|
|
}
|
|
|
|
func (dm *dashboardMetric) MadMultiplier() float64 {
|
|
medianValue, MAD := dm.MedianAbsoluteDeviation()
|
|
|
|
return absoluteValue(dm.Value-medianValue) / MAD
|
|
}
|
|
|
|
func (dm *dashboardMetric) StatisticalStatus() string {
|
|
mult := dm.MadMultiplier()
|
|
|
|
if mult > 4 {
|
|
return "Critical"
|
|
} else if mult > 3 {
|
|
return "Warning"
|
|
}
|
|
|
|
return "OK"
|
|
}
|
|
|
|
func (dm *dashboardMetric) PreferredStatus() string {
|
|
if dm.Meta.LastUpdate.Before(time.Now().Add(-1 * time.Duration(dm.Freshness) * time.Second)) {
|
|
return "Unknown"
|
|
}
|
|
|
|
if dm.IgnoreMAD {
|
|
return dm.Status
|
|
}
|
|
|
|
return dm.StatisticalStatus()
|
|
}
|
|
|
|
func (dm *dashboardMetric) LabelHistory() []string {
|
|
s := []string{}
|
|
|
|
labelStart := len(dm.HistoricalData) - 60
|
|
if labelStart < 0 {
|
|
labelStart = 0
|
|
}
|
|
|
|
for _, v := range dm.HistoricalData[labelStart:] {
|
|
s = append(s, strconv.Itoa(int(v.Time.Unix())))
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (dm *dashboardMetric) DataHistory() []string {
|
|
s := []string{}
|
|
|
|
dataStart := len(dm.HistoricalData) - 60
|
|
if dataStart < 0 {
|
|
dataStart = 0
|
|
}
|
|
|
|
for _, v := range dm.HistoricalData[dataStart:] {
|
|
s = append(s, strconv.FormatFloat(v.Value, 'g', 4, 64))
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (dm *dashboardMetric) Update(m *dashboardMetric) {
|
|
dm.Title = m.Title
|
|
dm.Description = m.Description
|
|
dm.Status = m.Status
|
|
dm.Value = m.Value
|
|
dm.IgnoreMAD = m.IgnoreMAD
|
|
dm.HideMAD = m.HideMAD
|
|
dm.HideValue = m.HideValue
|
|
if m.Expires != 0 {
|
|
dm.Expires = m.Expires
|
|
}
|
|
if m.Freshness != 0 {
|
|
dm.Freshness = m.Freshness
|
|
}
|
|
dm.HistoricalData = append(dm.HistoricalData, dashboardMetricStatus{
|
|
Time: time.Now(),
|
|
Status: m.Status,
|
|
Value: m.Value,
|
|
})
|
|
|
|
countStatus := make(map[string]float64)
|
|
|
|
expired := time.Now().Add(time.Duration(dm.Expires*-1) * time.Second)
|
|
tmp := dashboardMetricHistory{}
|
|
for _, s := range dm.HistoricalData {
|
|
if s.Time.After(expired) {
|
|
tmp = append(tmp, s)
|
|
countStatus[s.Status] = countStatus[s.Status] + 1
|
|
countStatus["Total"] = countStatus["Total"] + 1
|
|
if dm.Meta.LastOK.Before(s.Time) && s.Status == "OK" {
|
|
dm.Meta.LastOK = s.Time
|
|
}
|
|
}
|
|
}
|
|
dm.HistoricalData = tmp
|
|
|
|
dm.Meta.LastUpdate = time.Now()
|
|
if countStatus["Total"] > 0 {
|
|
dm.Meta.PercCrit = countStatus["Critical"] / countStatus["Total"] * 100
|
|
dm.Meta.PercWarn = countStatus["Warning"] / countStatus["Total"] * 100
|
|
dm.Meta.PercOK = countStatus["OK"] / countStatus["Total"] * 100
|
|
}
|
|
}
|
|
|
|
func (dm *dashboardMetric) IsValid() (bool, string) {
|
|
if dm.Expires > 604800 || dm.Expires < 0 {
|
|
return false, "Expires not in range 0 < x < 640800"
|
|
}
|
|
|
|
if dm.Freshness > 604800 || dm.Freshness < 0 {
|
|
return false, "Freshness not in range 0 < x < 640800"
|
|
}
|
|
|
|
if !stringInSlice(dm.Status, []string{"OK", "Warning", "Critical", "Unknowm"}) {
|
|
return false, "Status not allowed"
|
|
}
|
|
|
|
if len(dm.Title) > 512 || len(dm.Description) > 1024 {
|
|
return false, "Title or Description too long"
|
|
}
|
|
|
|
return true, ""
|
|
}
|
|
|
|
type historyBarSegment struct {
|
|
Duration time.Duration
|
|
End time.Time
|
|
Percentage float64
|
|
Start time.Time
|
|
Status string
|
|
}
|
|
|
|
func (dm dashboardMetric) GetHistoryBar() []historyBarSegment {
|
|
var (
|
|
point dashboardMetricStatus
|
|
segLength int
|
|
segments = []historyBarSegment{}
|
|
segStart time.Time
|
|
status = "Unknown"
|
|
)
|
|
|
|
for _, point = range dm.HistoricalData {
|
|
if point.Status == status {
|
|
segLength++
|
|
continue
|
|
}
|
|
|
|
// Store the old segment
|
|
if segLength > 0 {
|
|
segments = append(segments, historyBarSegment{
|
|
Duration: point.Time.Sub(segStart),
|
|
End: point.Time,
|
|
Percentage: float64(segLength) / float64(len(dm.HistoricalData)),
|
|
Start: segStart,
|
|
Status: status,
|
|
})
|
|
}
|
|
|
|
// Start a new segment
|
|
segLength = 1
|
|
segStart = point.Time
|
|
status = point.Status
|
|
}
|
|
|
|
segments = append(segments, historyBarSegment{
|
|
Duration: point.Time.Sub(segStart),
|
|
End: point.Time,
|
|
Percentage: float64(segLength) / float64(len(dm.HistoricalData)),
|
|
Start: segStart,
|
|
Status: status,
|
|
})
|
|
|
|
return segments
|
|
}
|