1
0
mirror of https://github.com/Luzifer/mondash.git synced 2024-09-19 17:02:58 +00:00

Initial version

This commit is contained in:
Knut Ahlers 2015-02-07 19:32:44 +01:00
commit 919e361353
8 changed files with 616 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
gin-bin

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# Luzifer / mondash
MonDash is a service for everyone having to display monitoring results to people who have not
the time or knowledge to get familar with Nagios / Icinga or similar monitoring systems. Therefore
MonDash provides a simple API to submit monitoring results and a simple dashboard to view those
results.
## Hosted
There is an instance of MonDash running on [mondash.org](https://mondash.org/) you can use for free. This means you can just head over there, create your own dashboard with one click and start to push your own metrics to your dashboard within 5 minutes. No registration, no fees, just your dashboard and you.
## Installation
However maybe you want to use MonDash for data you don't like to have public visible on the internet. As it is open source you also can host your own instance: The most simple way to install your own instance is to download a binary distribution on [gobuild.luzifer.io](http://gobuild.luzifer.io/github.com/Luzifer/mondash).
This archive will contain the binary you need to run your own instance and the template used for the display. If you want just edit the template, restart the daemon and you're done customizing MonDash. If you do so please do me one small favor: Include a hint to this repository / my instance.
MonDash needs some environment variables set when running:
+ `AWS_ACCESS_KEY_ID` - Your AWS Access-Key with access to the `S3Bucket`
+ `AWS_SECRET_ACCESS_KEY` - Your AWS Secret-Access-Key with access to the `S3Bucket`
+ `S3Bucket` - The S3 bucket used to store the dashboard metrics
+ `BASE_URL` - The Base-URL the application is running on for example `https://www.mondash.org`
+ `API_TOKEN` - API Token used for the /welcome dashboard (you can choose your own)
## Security
Just some words regarding security: MonDash was designed to be an open platform for creating dashboards without any hazzle. You just open a dashboard, send some data to it and you're already done. No need to think about OAuth or other authentication mechanisms.
The downpath of that concept is of course everyone can access every dashboard and see every data placed on it. So please don't use the public instances for private and/or secret data. You can just set up your own instance within 5 minutes (okay maybe 10 minutes if you want to do it right) and you can ensure that this instance is hidden from the internet.

97
apiary.apib Normal file
View File

@ -0,0 +1,97 @@
FORMAT: 1A
HOST: www.mondash.org
# MonDash
MonDash is a service for everyone having to display monitoring results to people who have not
the time or knowledge to get familar with Nagios / Icinga or similar monitoring systems. Therefore
MonDash provides a simple API to submit monitoring results and a simple dashboard to view those
results.
For the API to work you will need the APIToken assigned to your dashboard. This token is displayed
on the dashboard itself as long as no metrics are available to display.
To start just create a [randomly named dashboard](http://www.mondash.org/create) or start with a
named dashboard by simply visiting http://www.mondash.org/mydashboardname (if you plan to use the
named version please pay attention this will be easily guessable and you data is lesser protected
than with the random naming.
## Dashboard [/{dashid}]
This API controls your dashboard itself
+ Parameters
+ dashid (required, string, `098f6bcd4621d373cade`) ... The id of your dashboard to be found in the URL
### Delete your dashboard [DELETE]
This request will delete all your monitoring results available on your dashboard and release the
dashboard URL to the public.
_Please pay attention your dashboard URL will be available for others to register immediately
as we are not storing any data beyond this DELETE request._
+ Request
+ Header
Authorization: MyAPIToken
+ Response 200 (text/plain)
OK
## Metric [/{dashid}/{metricid}]
This API controls the metrics on your dashboard
+ Parameters
+ dashid (required, string, `098f6bcd4621d373cade`) ... The id of your dashboard to be found in the URL
+ metricid (required, string, `beer_available`) ... The unique name for your metric
### Submit a monitoring result [PUT]
+ Request (application/json)
+ Header
Authorization: MyAPIToken
+ Body
{
"title": "Amount of beer in the fridge",
"description": "Currently there are 12 bottles of beer in the fridge",
"status": "OK",
"expires": 604800,
"freshness": 3600
}
+ Attributes (object)
+ title (required, string) - The title of the metric to display on the dashboard
+ description (required, string) - A descriptive text for the current state of the metric
+ status (required, enum[string])
+ `OK`
+ `Warning`
+ `Critical`
+ `Unknown`
+ expires: 604800 (optional, number) - Time in seconds when to remove the metric if there is no update (Valid: `0 < x < 604800`)
+ freshness: 3600 (optional, number) - Time in seconds when to switch to `Unkown` state of there is no update (Valid: `0 < x < 604800`)
+ Response 200 (text/plain)
+ Body
OK
### Delete a metric from your dashboard [DELETE]
+ Request
+ Header
Authorization: MyAPIToken
+ Response 200 (text/plain)
+ Body
OK

184
main.go Normal file
View File

@ -0,0 +1,184 @@
package main
import (
"crypto/md5"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"time"
"log"
"launchpad.net/goamz/aws"
"launchpad.net/goamz/s3"
"github.com/flosch/pongo2"
"github.com/go-martini/martini"
_ "github.com/flosch/pongo2-addons"
)
var templates = make(map[string]*pongo2.Template)
var s3Storage *s3.Bucket
func main() {
preloadTemplates()
// Initialize S3 storage
awsAuth, err := aws.EnvAuth()
if err != nil {
log.Fatal(err)
}
s3Conn := s3.New(awsAuth, aws.EUWest)
s3Storage = s3Conn.Bucket(os.Getenv("S3Bucket"))
m := martini.Classic()
// Assets are in assets folder
m.Use(martini.Static("assets", martini.StaticOptions{Prefix: "/assets"}))
// Real handlers
m.Get("/", func(res http.ResponseWriter, req *http.Request) {
http.Redirect(res, req, "/welcome", 302)
})
m.Get("/create", func(res http.ResponseWriter, req *http.Request) {
urlProposal := generateAPIKey()[0:20]
_, err := s3Storage.Get(urlProposal)
for err == nil {
urlProposal = generateAPIKey()[0:20]
_, err = s3Storage.Get(urlProposal)
}
http.Redirect(res, req, fmt.Sprintf("/%s", urlProposal), http.StatusTemporaryRedirect)
})
m.Get("/:dashid", func(params martini.Params, res http.ResponseWriter) {
dash, err := LoadDashboard(params["dashid"])
if err != nil {
dash = &Dashboard{APIKey: generateAPIKey(), Metrics: DashboardMetrics{}}
}
sort.Sort(sort.Reverse(DashboardMetrics(dash.Metrics)))
renderTemplate("dashboard.html", pongo2.Context{
"dashid": params["dashid"],
"metrics": dash.Metrics,
"apikey": dash.APIKey,
"baseurl": os.Getenv("BASE_URL"),
}, res)
})
m.Delete("/:dashid", func(params martini.Params, req *http.Request, res http.ResponseWriter) {
dash, err := LoadDashboard(params["dashid"])
if err != nil {
http.Error(res, "This dashboard does not exist.", http.StatusInternalServerError)
return
}
if dash.APIKey != req.Header.Get("Authorization") {
http.Error(res, "APIKey did not match.", http.StatusUnauthorized)
return
}
s3Storage.Del(params["dashid"])
http.Error(res, "OK", http.StatusOK)
})
m.Put("/:dashid/:metricid", func(params martini.Params, req *http.Request, res http.ResponseWriter) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
http.Error(res, "Internal Server Error", http.StatusInternalServerError)
return
}
metricUpdate := NewDashboardMetric()
err = json.Unmarshal(body, metricUpdate)
if err != nil {
http.Error(res, "Unable to unmarshal json", http.StatusInternalServerError)
return
}
dash, err := LoadDashboard(params["dashid"])
if err != nil {
dash = &Dashboard{APIKey: req.Header.Get("Authorization"), Metrics: DashboardMetrics{}, DashboardID: params["dashid"]}
}
if dash.APIKey != req.Header.Get("Authorization") {
http.Error(res, "APIKey did not match.", http.StatusUnauthorized)
return
}
updated := false
for _, m := range dash.Metrics {
if m.MetricID == params["metricid"] {
m.Update(metricUpdate)
updated = true
break
}
}
if !updated {
tmp := &DashboardMetric{MetricID: params["metricid"]}
tmp.Update(metricUpdate)
dash.Metrics = append(dash.Metrics, tmp)
}
dash.Save()
http.Error(res, "OK", http.StatusOK)
})
m.Delete("/:dashid/:metricid", func(params martini.Params, req *http.Request, res http.ResponseWriter) {
dash, err := LoadDashboard(params["dashid"])
if err != nil {
dash = &Dashboard{APIKey: req.Header.Get("Authorization"), Metrics: DashboardMetrics{}, DashboardID: params["dashid"]}
}
if dash.APIKey != req.Header.Get("Authorization") {
http.Error(res, "APIKey did not match.", http.StatusUnauthorized)
return
}
tmp := DashboardMetrics{}
for _, m := range dash.Metrics {
if m.MetricID != params["metricid"] {
tmp = append(tmp, m)
}
}
dash.Metrics = tmp
dash.Save()
http.Error(res, "OK", http.StatusOK)
})
go RunWelcomePage()
// GO!
m.Run()
}
func generateAPIKey() string {
t := time.Now().String()
sum := md5.Sum([]byte(t))
return fmt.Sprintf("%x", sum)
}
func renderTemplate(templateName string, context pongo2.Context, res http.ResponseWriter) {
if tpl, ok := templates[templateName]; ok {
tpl.ExecuteWriter(context, res)
} else {
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("Template %s not found!", templateName)))
}
}
func preloadTemplates() {
templateNames, err := ioutil.ReadDir("templates")
if err != nil {
panic("Templates directory not available!")
}
for _, tplname := range templateNames {
templates[tplname.Name()] = pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s", tplname.Name())))
}
}

10
strings.go Normal file
View File

@ -0,0 +1,10 @@
package main
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}

139
structs.go Normal file
View File

@ -0,0 +1,139 @@
package main
import (
"encoding/json"
"errors"
"log"
"time"
"launchpad.net/goamz/s3"
)
type Dashboard struct {
DashboardID string `json:"-"`
APIKey string `json:"api_key"`
Metrics DashboardMetrics `json:"metrics"`
}
func LoadDashboard(dashid string) (*Dashboard, error) {
data, err := s3Storage.Get(dashid)
if err != nil {
return &Dashboard{}, errors.New("Dashboard not found")
}
tmp := &Dashboard{DashboardID: dashid}
json.Unmarshal(data, tmp)
return tmp, nil
}
func (d *Dashboard) Save() {
data, err := json.Marshal(d)
if err != nil {
log.Println(err)
}
err = s3Storage.Put(d.DashboardID, data, "application/json", s3.Private)
if err != nil {
log.Println(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].HistoricalData[0].Time.Before(a[j].HistoricalData[0].Time)
}
type DashboardMetric struct {
MetricID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Expires int64 `json:"expires,omitifempty"`
Freshness int64 `json:"freshness,omitifempty"`
HistoricalData DashboardMetricHistory `json:"history,omitifempty"`
Meta DashboardMetricMeta `json:"meta,omitifempty"`
}
type DashboardMetricStatus struct {
Time time.Time `json:"time"`
Status string `json:"status"`
}
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{},
}
}
func (dm *DashboardMetric) Update(m *DashboardMetric) {
dm.Title = m.Title
dm.Description = m.Description
dm.Status = m.Status
if m.Expires != 0 {
dm.Expires = m.Expires
}
if m.Freshness != 0 {
dm.Freshness = m.Freshness
}
dm.HistoricalData = append(DashboardMetricHistory{DashboardMetricStatus{
Time: time.Now(),
Status: m.Status,
}}, dm.HistoricalData...)
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()
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, ""
}

103
templates/dashboard.html Normal file
View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MonDash - Dashboard</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">MonDash</a>
</div><!-- /.navbar-header -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><a href="/create">Get your own dashboard</a></li>
</ul><!-- /.navbar-right -->
</div><!-- /.navbar-collapse -->
</div>
</nav>
<div class="container">
{% if dashid == "welcome" %}
<div class="row">
<div class="jumbotron text-center">
<h1>Welcome to MonDash!</h1>
<p>You're currently seeing a demo dashboard updated with random numbers below. To get started read the <a href="http://docs.mondash.apiary.io/" target="_blank">API documentation</a> and create your own dashboard by clicking the button in the upper right hand corner&hellip;
<p>If you have any questions about this project don't hesitate to ask <a href="https://luzifer.io/" target="_blank">Knut</a>.</p>
</div>
</div>
{% endif %}
{% if metrics|length == 0 and dashid != "welcome" %}
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<p>Welcome to your new dashboard. Your API-key is:</p>
<pre>{{ apikey }}</pre>
<p>After you sent your first metric you can reach your dashboard here:</p>
<a href="{{ baseurl }}/{{ dashid }}">{{ baseurl }}/{{ dashid }}</a>
</div>
</div>
{% else %}
{% for metric in metrics %}
{% if metric.Status == "OK" %}
<div class="row alert alert-success">
{% elif metric.Status == "Warning" %}
<div class="row alert alert-warning">
{% elif metric.Status == "Critical" %}
<div class="row alert alert-danger">
{% else %}
<div class="row alert alert-info">
{% endif %}
<div class="col-md-9">
<h4>{{ metric.Title }}</h4>
<p>{{ metric.Description }}</p>
<small>
Updated {{ metric.Meta.LastUpdate|naturaltime}}
{% if metric.Status != "OK" %}
/ Last ok {{ metric.Meta.LastOK|naturaltime }}
{% endif %}
</small>
</div>
<div class="col-md-3 hidden-xs">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{ metric.Meta.PercOK }}%">
</div>
<div class="progress-bar progress-bar-warning" style="width: {{ metric.Meta.PercWarn }}%">
</div>
<div class="progress-bar progress-bar-danger" style="width: {{ metric.Meta.PercCrit }}%">
</div>
</div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
</body>
</html>

52
welcome_runner.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"time"
)
func RunWelcomePage() {
baseURL := os.Getenv("BASE_URL")
welcomeAPIToken := os.Getenv("API_TOKEN")
generateTicker := time.NewTicker(time.Minute)
for {
select {
case <-generateTicker.C:
beers := rand.Intn(24)
status := "OK"
switch {
case beers < 6:
status = "Critical"
break
case beers < 12:
status = "Warning"
break
}
beer := DashboardMetric{
Title: "Amount of beer in the fridge",
Description: fmt.Sprintf("Currently there are %d bottles of beer in the fridge", beers),
Status: status,
Expires: 86400,
Freshness: 120,
}
body, err := json.Marshal(beer)
if err != nil {
log.Println(err)
}
url := fmt.Sprintf("%s/welcome/beer_available", baseURL)
req, _ := http.NewRequest("PUT", url, bytes.NewBuffer(body))
req.Header.Add("Authorization", welcomeAPIToken)
http.DefaultClient.Do(req)
}
}
}