2015-02-20 18:58:16 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2019-05-24 22:03:06 +00:00
|
|
|
"mime"
|
2015-02-20 18:58:16 +00:00
|
|
|
"net/http"
|
2019-05-24 22:03:06 +00:00
|
|
|
"os"
|
|
|
|
"path"
|
2015-02-20 18:58:16 +00:00
|
|
|
"sort"
|
2019-05-24 22:03:06 +00:00
|
|
|
"strings"
|
2015-02-20 18:58:16 +00:00
|
|
|
"time"
|
|
|
|
|
2015-07-06 20:23:46 +00:00
|
|
|
"github.com/gorilla/mux"
|
2019-05-24 22:03:06 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
log "github.com/sirupsen/logrus"
|
2015-02-20 18:58:16 +00:00
|
|
|
)
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
type output struct {
|
|
|
|
APIKey string `json:"api_key,omitempty"`
|
|
|
|
Metrics []outputMetric `json:"metrics"`
|
2015-02-20 18:58:16 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
type outputMetricConfig struct {
|
|
|
|
HideMAD bool `json:"hide_mad"`
|
|
|
|
HideValue bool `json:"hide_value"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type outputMetric struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
Config outputMetricConfig `json:"config"`
|
|
|
|
Description string `json:"description"`
|
2019-08-16 21:29:01 +00:00
|
|
|
DetailURL string `json:"detail_url"`
|
2019-05-24 22:03:06 +00:00
|
|
|
HistoryBar []historyBarSegment `json:"history_bar,omitempty"`
|
|
|
|
LastOK time.Time `json:"last_ok"`
|
|
|
|
LastUpdate time.Time `json:"last_update"`
|
|
|
|
Median float64 `json:"median"`
|
|
|
|
MADMultiplier float64 `json:"mad_multiplier"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
Title string `json:"title"`
|
|
|
|
Value float64 `json:"value"`
|
|
|
|
ValueHistory map[int64]float64 `json:"value_history,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type outputMetricFromMetricOpts struct {
|
|
|
|
Metric *dashboardMetric
|
|
|
|
AddHistoryBar bool
|
|
|
|
AddValueHistory bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func outputMetricFromMetric(opts outputMetricFromMetricOpts) outputMetric {
|
|
|
|
out := outputMetric{
|
|
|
|
ID: opts.Metric.MetricID,
|
|
|
|
Description: opts.Metric.Description,
|
2019-08-16 21:29:01 +00:00
|
|
|
DetailURL: opts.Metric.DetailURL,
|
2019-05-24 22:03:06 +00:00
|
|
|
LastOK: opts.Metric.Meta.LastOK,
|
|
|
|
LastUpdate: opts.Metric.Meta.LastUpdate,
|
|
|
|
Median: opts.Metric.Median(),
|
|
|
|
MADMultiplier: opts.Metric.MadMultiplier(),
|
|
|
|
Status: opts.Metric.PreferredStatus(),
|
|
|
|
Title: opts.Metric.Title,
|
|
|
|
Value: opts.Metric.Value,
|
|
|
|
|
|
|
|
Config: outputMetricConfig{
|
|
|
|
HideMAD: opts.Metric.HideMAD,
|
|
|
|
HideValue: opts.Metric.HideValue,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.AddHistoryBar {
|
|
|
|
out.HistoryBar = opts.Metric.GetHistoryBar()
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.AddValueHistory {
|
|
|
|
out.ValueHistory = opts.Metric.HistoricalValueMap()
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleStaticFile(w http.ResponseWriter, r *http.Request, filename string) error {
|
|
|
|
if _, err := os.Stat(path.Join(cfg.FrontendDir, filename)); err == nil {
|
|
|
|
http.ServeFile(w, r, path.Join(cfg.FrontendDir, filename))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// File was not found in filesystem, serve from packed assets
|
|
|
|
body, err := Asset(path.Join("frontend", filename))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "File %q was neither found in FrontendDir nor in assets", filename)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.WithField("filename", filename).Debug("Static file loaded from assets")
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", mime.TypeByExtension(filename[strings.LastIndexByte(filename, '.'):]))
|
|
|
|
_, err = w.Write(body)
|
|
|
|
return errors.Wrap(err, "Unable to write body")
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleAppJS(w http.ResponseWriter, r *http.Request) { handleStaticFile(w, r, "app.js") }
|
|
|
|
|
|
|
|
func handleCreateRandomDashboard(w http.ResponseWriter, r *http.Request) {
|
2015-07-06 19:41:21 +00:00
|
|
|
var urlProposal string
|
|
|
|
for {
|
2015-02-20 18:58:16 +00:00
|
|
|
urlProposal = generateAPIKey()[0:20]
|
2015-07-06 19:41:21 +00:00
|
|
|
if exists, err := store.Exists(urlProposal); err == nil && !exists {
|
|
|
|
break
|
|
|
|
}
|
2015-02-20 18:58:16 +00:00
|
|
|
}
|
2019-05-24 22:03:06 +00:00
|
|
|
http.Redirect(w, r, fmt.Sprintf("/%s", urlProposal), http.StatusTemporaryRedirect)
|
2015-02-20 18:58:16 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
func handleDisplayDashboard(w http.ResponseWriter, r *http.Request) {
|
|
|
|
handleStaticFile(w, r, "index.html")
|
|
|
|
}
|
2015-02-20 18:58:16 +00:00
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
func handleDisplayDashboardJSON(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var vars = mux.Vars(r)
|
2015-02-20 18:58:16 +00:00
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
dash, err := loadDashboard(vars["dashid"], store)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// All fine
|
2015-02-20 18:58:16 +00:00
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
case errDashboardNotFound:
|
|
|
|
dash = &dashboard{APIKey: generateAPIKey(), Metrics: []*dashboardMetric{}}
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.WithError(err).
|
|
|
|
WithField("dashboard_id", vars["dashid"]).
|
|
|
|
Error("Unable to load dashboard")
|
|
|
|
http.Error(w, "Could not load dashboard", http.StatusInternalServerError)
|
|
|
|
return
|
2016-03-27 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
var (
|
|
|
|
addHistoryBar = r.URL.Query().Get("history_bar") == "true"
|
|
|
|
addValueHistory = r.URL.Query().Get("value_history") == "true"
|
|
|
|
response = output{}
|
|
|
|
)
|
2016-03-27 20:18:13 +00:00
|
|
|
|
|
|
|
// Filter out expired metrics
|
|
|
|
for _, m := range dash.Metrics {
|
|
|
|
if m.Meta.LastUpdate.After(time.Now().Add(time.Duration(m.Expires*-1) * time.Second)) {
|
2019-05-24 22:03:06 +00:00
|
|
|
response.Metrics = append(response.Metrics, outputMetricFromMetric(outputMetricFromMetricOpts{
|
|
|
|
AddHistoryBar: addHistoryBar,
|
|
|
|
AddValueHistory: addValueHistory,
|
|
|
|
Metric: m,
|
|
|
|
}))
|
2016-03-27 20:18:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
sort.Slice(response.Metrics, func(j, i int) bool { return response.Metrics[i].LastUpdate.Before(response.Metrics[j].LastUpdate) })
|
|
|
|
|
2016-03-27 20:18:13 +00:00
|
|
|
if len(response.Metrics) == 0 {
|
|
|
|
response.APIKey = dash.APIKey
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
|
|
|
|
if err = json.NewEncoder(w).Encode(response); err != nil {
|
|
|
|
log.WithError(err).Error("Unable to encode API JSON")
|
|
|
|
http.Error(w, "Unable to encode JSON", http.StatusInternalServerError)
|
|
|
|
}
|
2016-03-27 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
func handleDeleteDashboard(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Token ")
|
|
|
|
vars = mux.Vars(r)
|
|
|
|
)
|
|
|
|
|
|
|
|
dash, err := loadDashboard(vars["dashid"], store)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// All fine
|
|
|
|
|
|
|
|
case errDashboardNotFound:
|
|
|
|
http.Error(w, "Dasboard not found", http.StatusNotFound)
|
|
|
|
return
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.WithError(err).
|
|
|
|
WithField("dashboard_id", vars["dashid"]).
|
|
|
|
Error("Unable to load dashboard")
|
|
|
|
http.Error(w, "Could not load dashboard", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if dash.APIKey != token {
|
|
|
|
http.Error(w, "APIKey did not match.", http.StatusUnauthorized)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
if err = store.Delete(vars["dashid"]); err != nil {
|
|
|
|
log.WithError(err).WithField("dashboard_id", vars["dashid"]).Error("Unable to delete dashboard")
|
|
|
|
http.Error(w, "Failed to delete dashboard", http.StatusInternalServerError)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
http.Error(w, "OK", http.StatusOK)
|
2015-02-20 18:58:16 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
func handleDeleteMetric(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Token ")
|
|
|
|
vars = mux.Vars(r)
|
|
|
|
)
|
|
|
|
|
|
|
|
dash, err := loadDashboard(vars["dashid"], store)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// All fine
|
|
|
|
|
|
|
|
case errDashboardNotFound:
|
|
|
|
http.Error(w, "Dashboard not found", http.StatusNotFound)
|
|
|
|
return
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.WithError(err).
|
|
|
|
WithField("dashboard_id", vars["dashid"]).
|
|
|
|
Error("Unable to load dashboard")
|
|
|
|
http.Error(w, "Could not load dashboard", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if dash.APIKey != token {
|
|
|
|
http.Error(w, "APIKey did not match.", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tmp := []*dashboardMetric{}
|
|
|
|
for _, m := range dash.Metrics {
|
|
|
|
if m.MetricID != vars["metricid"] {
|
|
|
|
tmp = append(tmp, m)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
dash.Metrics = tmp
|
|
|
|
|
|
|
|
if err := dash.Save(); err != nil {
|
|
|
|
log.WithError(err).Error("Unable to save dashboard")
|
|
|
|
http.Error(w, "Was not able to save the dashboard", http.StatusInternalServerError)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
http.Error(w, "OK", http.StatusOK)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handlePutMetric(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Token ")
|
|
|
|
vars = mux.Vars(r)
|
|
|
|
)
|
|
|
|
|
2015-02-20 18:58:16 +00:00
|
|
|
metricUpdate := newDashboardMetric()
|
2019-05-24 22:03:06 +00:00
|
|
|
if err := json.NewDecoder(r.Body).Decode(metricUpdate); err != nil {
|
|
|
|
http.Error(w, "Unable to unmarshal json body", http.StatusBadRequest)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
dash, err := loadDashboard(vars["dashid"], store)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
// All fine
|
|
|
|
|
|
|
|
case errDashboardNotFound:
|
|
|
|
// Dashboard may be created with first metrics put
|
|
|
|
if len(token) < 10 {
|
|
|
|
http.Error(w, "APIKey is too insecure", http.StatusBadRequest)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
2019-05-24 22:03:06 +00:00
|
|
|
|
2015-07-06 21:07:04 +00:00
|
|
|
dash = &dashboard{
|
2019-05-24 22:03:06 +00:00
|
|
|
APIKey: token,
|
|
|
|
Metrics: []*dashboardMetric{},
|
|
|
|
DashboardID: vars["dashid"],
|
2015-07-06 21:07:04 +00:00
|
|
|
storage: store,
|
|
|
|
}
|
2019-05-24 22:03:06 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
log.WithError(err).
|
|
|
|
WithField("dashboard_id", vars["dashid"]).
|
|
|
|
Error("Unable to load dashboard")
|
|
|
|
http.Error(w, "Could not load dashboard", http.StatusInternalServerError)
|
|
|
|
return
|
2015-02-20 18:58:16 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
if dash.APIKey != token {
|
|
|
|
http.Error(w, "APIKey did not match.", http.StatusUnauthorized)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
valid, reason := metricUpdate.IsValid()
|
|
|
|
if !valid {
|
2019-05-24 22:03:06 +00:00
|
|
|
http.Error(w, fmt.Sprintf("Invalid data: %s", reason), http.StatusBadRequest)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
updated := false
|
|
|
|
for _, m := range dash.Metrics {
|
2019-05-24 22:03:06 +00:00
|
|
|
if m.MetricID == vars["metricid"] {
|
2015-02-20 18:58:16 +00:00
|
|
|
m.Update(metricUpdate)
|
|
|
|
updated = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !updated {
|
|
|
|
tmp := newDashboardMetric()
|
2019-05-24 22:03:06 +00:00
|
|
|
tmp.MetricID = vars["metricid"]
|
2015-02-20 18:58:16 +00:00
|
|
|
tmp.Update(metricUpdate)
|
|
|
|
dash.Metrics = append(dash.Metrics, tmp)
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
if err := dash.Save(); err != nil {
|
|
|
|
log.WithError(err).Error("Unable to save dashboard")
|
|
|
|
http.Error(w, "Was not able to save the dashboard", http.StatusInternalServerError)
|
2015-02-20 18:58:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
http.Error(w, "OK", http.StatusOK)
|
|
|
|
}
|
2015-02-20 18:58:16 +00:00
|
|
|
|
2019-05-24 22:03:06 +00:00
|
|
|
func handleRedirectWelcome(w http.ResponseWriter, r *http.Request) {
|
|
|
|
http.Redirect(w, r, "/welcome", http.StatusTemporaryRedirect)
|
2015-02-20 18:58:16 +00:00
|
|
|
}
|