mirror of
https://github.com/Luzifer/mondash.git
synced 2024-12-22 20:11:18 +00:00
333 lines
7.7 KiB
Go
333 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/Luzifer/mondash/storage"
|
|
)
|
|
|
|
const defaultStalenessStatus = "Unknown"
|
|
|
|
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"`
|
|
StalenessStatus string `json:"staleness_status,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: defaultStalenessStatus,
|
|
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)) {
|
|
if dm.StalenessStatus == "" {
|
|
return defaultStalenessStatus
|
|
}
|
|
return dm.StalenessStatus
|
|
}
|
|
|
|
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 = defaultStalenessStatus
|
|
)
|
|
|
|
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
|
|
}
|