monfront - add check detail view

This adds a detail view for a single check. The purpose is to view
notifications for this check alone and get the context information on
the node it belongs to, the command and settings.
This commit is contained in:
Gibheer 2019-01-09 20:17:49 +01:00
parent 3b222e06ed
commit 96d853fad6

View File

@ -30,8 +30,11 @@ type (
Context struct { Context struct {
Title string Title string
CurrentPath string
Error string
Mappings map[int]map[int]MapEntry Mappings map[int]map[int]MapEntry
Checks []check Checks []check
CheckDetails *checkDetails
Groups []group Groups []group
} }
@ -54,6 +57,42 @@ type (
Msg string Msg string
} }
checkDetails struct {
Id int64
Message string
Enabled bool
Updated time.Time
LastRefresh time.Time
NextTime time.Time
MappingId int
MappingName string
NodeId int
NodeName string
NodeMessage string
CommandId int
CommandName string
CommandLine []string
CommandMessage string
States []int64
Notice sql.NullString
Notifiers []notifier
Notifications []notification
}
notifier struct {
Id int
Name string
Enabled bool
}
notification struct {
Id int64
States []int
Output string
Inserted time.Time
Sent time.Time
}
group struct { group struct {
GroupId int GroupId int
Name string Name string
@ -90,7 +129,10 @@ func main() {
Tmpl = tmpl Tmpl = tmpl
http.HandleFunc("/", showChecks) http.HandleFunc("/", showChecks)
http.HandleFunc("/static/", showStatic)
http.HandleFunc("/check", showCheck) http.HandleFunc("/check", showCheck)
http.HandleFunc("/group", showGroup)
http.HandleFunc("/node", showNode)
http.HandleFunc("/checks", showChecks) http.HandleFunc("/checks", showChecks)
http.HandleFunc("/groups", showGroups) http.HandleFunc("/groups", showGroups)
http.HandleFunc("/action", checkAction) http.HandleFunc("/action", checkAction)
@ -100,6 +142,7 @@ func main() {
} }
func checkAction(w http.ResponseWriter, r *http.Request) { func checkAction(w http.ResponseWriter, r *http.Request) {
con := Context{}
if r.Method != "POST" { if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("method is not supported")) w.Write([]byte("method is not supported"))
@ -157,8 +200,8 @@ func checkAction(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusSeeOther) w.WriteHeader(http.StatusSeeOther)
return return
default: default:
w.WriteHeader(http.StatusNotFound) con.Error = fmt.Sprintf("requested action '%s' does not exist", action[0])
fmt.Fprintf(w, "unknown action %s", action) returnError(http.StatusNotFound, con, w)
return return
} }
whereColumn := "id" whereColumn := "id"
@ -194,7 +237,7 @@ func showChecks(w http.ResponseWriter, r *http.Request) {
left join ( select distinct check_id from checks_notify where enabled = true) cn on c.id = cn.check_id` left join ( select distinct check_id from checks_notify where enabled = true) cn on c.id = cn.check_id`
where := []string{} where := []string{}
if strings.HasPrefix(r.URL.Path, "/unhandled") { if strings.HasPrefix(r.URL.Path, "/unhandled") {
where = append(where, `ac.states[1] > 0 and ac.acknowledged = false`) where = append(where, `ac.states[1] > 0 and ac.acknowledged = false and ac.enabled = true`)
} }
idx := 0 idx := 0
params := []interface{}{} params := []interface{}{}
@ -252,6 +295,7 @@ func showChecks(w http.ResponseWriter, r *http.Request) {
Checks: checks, Checks: checks,
} }
if err := loadMappings(&con); err != nil { if err := loadMappings(&con); err != nil {
con.Error = "could not load mapping data"
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problem with the mappings")) w.Write([]byte("problem with the mappings"))
log.Printf("could not load mappings: %s", err) log.Printf("could not load mappings: %s", err)
@ -267,10 +311,6 @@ func showChecks(w http.ResponseWriter, r *http.Request) {
return return
} }
// showCheck loads shows the notifications for a specific check.
func showCheck(w http.ResponseWriter, r *http.Request) {
}
func showGroups(w http.ResponseWriter, r *http.Request) { func showGroups(w http.ResponseWriter, r *http.Request) {
query := `select groupid, groupname, nodeid, nodename, mapping_id, state query := `select groupid, groupname, nodeid, nodename, mapping_id, state
from ( from (
@ -326,6 +366,73 @@ func showGroups(w http.ResponseWriter, r *http.Request) {
return return
} }
// showCheck loads shows the notifications for a specific check.
func showCheck(w http.ResponseWriter, r *http.Request) {
cd := checkDetails{}
con := Context{CheckDetails: &cd}
id, found := r.URL.Query()["check_id"]
if !found {
con.Error = "no check given to view"
returnError(http.StatusNotFound, con, w)
return
}
query := `select c.id, c.message, c.enabled, c.updated, c.last_refresh,
m.id, m.name, n.id, n.name, n.message, co.id, co.Name, co.message,
ac.cmdline, ac.states, ac.msg
from checks c
join active_checks ac on c.id = ac.check_id
join nodes n on c.node_id = n.id
join commands co on c.command_id = co.id
join mappings m on ac.mapping_id = m.id
where c.id = $1::bigint`
err := DB.QueryRow(query, id[0]).Scan(&cd.Id, &cd.Message, &cd.Enabled, &cd.Updated, &cd.LastRefresh,
&cd.MappingId, &cd.MappingName, &cd.NodeId, &cd.NodeName, &cd.NodeMessage,
&cd.CommandId, &cd.CommandName, &cd.CommandMessage,
pq.Array(&cd.CommandLine), pq.Array(&cd.States), &cd.Notice)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problems with the database"))
log.Printf("could not get check details for check id %s: %s", id[0], err)
return
}
// TODO load the last couple notifications
if err := loadMappings(&con); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problem with the mappings"))
log.Printf("could not load mappings: %s", err)
return
}
w.Header()["Content-Type"] = []string{"text/html"}
if err := Tmpl.ExecuteTemplate(w, "check", con); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problem with a template"))
log.Printf("could not execute template: %s", err)
return
}
}
func showGroup(w http.ResponseWriter, r *http.Request) {
// TODO implement showing all nodes only from one group and its message?
return
}
func showNode(w http.ResponseWriter, r *http.Request) {
// TODO implement showing all checks from one node and its message?
return
}
func returnError(status int, con Context, w http.ResponseWriter) {
w.Header()["Content-Type"] = []string{"text/html"}
w.WriteHeader(status)
if err := Tmpl.ExecuteTemplate(w, "error", con); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problem with a template"))
log.Printf("could not execute template: %s", err)
}
}
func loadMappings(c *Context) error { func loadMappings(c *Context) error {
c.Mappings = map[int]map[int]MapEntry{} c.Mappings = map[int]map[int]MapEntry{}
rows, err := DB.Query(SQLShowMappings) rows, err := DB.Query(SQLShowMappings)
@ -356,6 +463,20 @@ func loadMappings(c *Context) error {
return nil return nil
} }
func showStatic(w http.ResponseWriter, r *http.Request) {
file := strings.TrimPrefix(r.URL.Path, "/static/")
raw, found := Static[file]
if !found {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("file does not exist"))
return
}
w.Header()["Content-Type"] = []string{"image/svg+xml"}
w.WriteHeader(http.StatusOK)
w.Write([]byte(raw))
return
}
var ( var (
SQLShowMappings = `select mapping_id, target, title, color SQLShowMappings = `select mapping_id, target, title, color
from mapping_level` from mapping_level`
@ -367,6 +488,7 @@ var (
<html> <html>
<head> <head>
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<link rel="shortcut icon" href="/static/favicon" />
<style type="text/css"> <style type="text/css">
* { font-size: 100%; } * { font-size: 100%; }
body { display: flex; flex-direction: column; padding: 0; margin: 0; } body { display: flex; flex-direction: column; padding: 0; margin: 0; }
@ -409,14 +531,26 @@ var (
table tr:hover { background: #dfdfdf; } table tr:hover { background: #dfdfdf; }
table th { background: #cccccc; color: #3a5f78; } table th { background: #cccccc; color: #3a5f78; }
table td, table th { text-align: center; } table td, table th { text-align: center; }
table pre { font-size: 75%; } table code { font-size: 75%; }
table td.disabled { text-decoration: line-through; } table td.disabled { text-decoration: line-through; }
.icon { height: 1em; margin: 0; width: 1em; vertical-align: bottom; margin-right: 0.5em;} .icon {
{{ range $mapId, $mapping := .Mappings }} display: inline-block;
{{ range $target, $val := $mapping }} height: 1em;
td.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; } margin: 0;
{{ end }} width: 1em;
{{ end }} vertical-align: bottom;
margin-right: 0.5em;
background-size: contain;
}
.mute {
background-image: url(/static/icon-mute);
}
/* state background colors */
{{ range $mapId, $mapping := .Mappings -}}
{{ range $target, $val := $mapping -}}
.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
{{ end -}}
{{ end -}}
</style> </style>
<script> <script>
setTimeout(function() { if (document.activeElement.tagName == "BODY") { location.reload(true) } }, 30000) setTimeout(function() { if (document.activeElement.tagName == "BODY") { location.reload(true) } }, 30000)
@ -448,7 +582,8 @@ var (
</li> </li>
<li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li> <li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li>
</ul> </ul>
</nav>`, </nav>
{{ if .Error }}<div class="error">{{ .Error }}</div>{{ end }}`,
"footer": `</body></html>`, "footer": `</body></html>`,
"checklist": `{{ template "header" . }} "checklist": `{{ template "header" . }}
<form method="post" action="/action"> <form method="post" action="/action">
@ -482,9 +617,9 @@ var (
<tr> <tr>
<td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td> <td><input type="checkbox" name="checks" value="{{ .CheckID }}" /></td>
<td>{{ if ne $current .NodeName }}{{ $current = .NodeName }}<a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a>{{ end }}</td> <td>{{ if ne $current .NodeName }}{{ $current = .NodeName }}<a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a>{{ end }}</td>
<td class="state-{{ .MappingId }}-{{ .State }}">{{ if ne .Notify true }}{{ template "icon-mute" . }}{{ end }}{{ .CommandName }} - {{ (index $mapping .MappingId .State).Title }}</td> <td class="state-{{ .MappingId }}-{{ .State }}">{{ if ne .Notify true }}<span class="icon mute"></span>{{ end }}<a href="/check?check_id={{ .CheckID }}">{{ .CommandName }}</a> - {{ (index $mapping .MappingId .State).Title }}</td>
<td {{ if ne .Enabled true }}title="This check is disabled." class="disabled"{{ end }}>{{ .NextTime.Format "2006.01.02 15:04:05" }} - in {{ in .NextTime }}</td> <td {{ if ne .Enabled true }}title="This check is disabled." class="disabled"{{ end }}>{{ .NextTime.Format "2006.01.02 15:04:05" }} - in {{ in .NextTime }}</td>
<td><pre>{{ .Msg }}</pre></td> <td><code>{{ .Msg }}</code></td>
</tr> </tr>
{{ end }} {{ end }}
</tbody> </tbody>
@ -492,8 +627,7 @@ var (
</content> </content>
</form> </form>
{{ template "footer" . }}`, {{ template "footer" . }}`,
"grouplist": `{{ template "header" . }} "checkformheader": `<form method="post" action="/action">
<form method="post" action="/action">
<section> <section>
<nav> <nav>
<div class="option"> <div class="option">
@ -513,7 +647,10 @@ var (
<input name="comment" /> <input name="comment" />
</div> </div>
<button type="submit">submit</button> <button type="submit">submit</button>
</nav> </nav>`,
"checkformfooter": `</section></form>`,
"grouplist": `{{ template "header" . }}
{{ template "checkformheader" . }}
<content> <content>
<table> <table>
<thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead> <thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead>
@ -531,14 +668,51 @@ var (
</tbody> </tbody>
</table> </table>
</content> </content>
</form> {{ template "checkformfooter" . }}
{{ template "footer" . }}`, {{ template "footer" . }}`,
"icon-mute": `<svg class="icon" width="100" height="100" viewBox="0 0 35.3 35.3" version="1.1"><title>Check is muted</title><style>.s0{fill:#191919;}</style><g transform="translate(0,-261.72223)"><path d="m17.6 261.7v35.3L5.3 284.7H0v-10.6l5.3 0zM30.2 273.1l-3.7 3.7-3.7-3.7-2.5 2.5 3.7 3.7-3.7 3.7 2.5 2.5 3.7-3.7 3.7 3.7 2.5-2.5-3.7-3.7 3.7-3.7z" fill="#191919"/></g></svg>`, "check": `{{ template "header" . }}
{{ template "checkformheader" . }}
<content>
{{ $mapping := .Mappings }}
{{ with .CheckDetails }}
<input type="hidden" name="check_id" value="{{ .Id }}" />
<article>
<h1>check</h1>
<div><span class="label">current state</span><span class="value state-{{ .MappingId }}-{{ index .States 0 }}">{{ (index $mapping .MappingId (index .States 0) ).Title }}</span></div>
<div><span class="label">current notice</span><span class="value">{{ if .Notice }}{{ .Notice.String }}{{ end }}</span></div>
<div><span class="label">Message</span><span class="value">{{ .Message }}</span></div>
<div><span class="label">enabled</span><span class="value">{{ .Enabled }}</span></div>
<div><span class="label">updated</span><span class="value">{{ .Updated.Format "2006.01.02 15:04:05" }}</span></div>
<div><span class="label">next check</span><span class="value">{{ .NextTime.Format "2006.01.02 15:04:05" }}</span></div>
<div><span class="label">last refresh</span><span class="value">{{ .LastRefresh.Format "2006.01.02 15:04:05" }}</span></div>
<div><span class="label">mapping</span><span class="value">{{ .MappingId }}</span></div>
</article>
<article>
<h1>node {{ .NodeName }}</h1>
<div><span class="label">Message</span><span class="value">{{ .NodeMessage }}</span></div>
</article>
<article>
<h1>command {{ .CommandName }}</h1>
<div><span class="label">Message</span><span class="value">{{ .CommandMessage }}</span></div>
<div><span class="label">command line</span><span class="value">{{ .CommandLine }}</span></div>
</article>
States []int64
Notifiers []notifier
Notifications []notification
{{ end }}
</content>
{{ template "checkformfooter" . }}
{{ template "footer" . }}`,
}
Static = map[string]string{
"icon-mute": `<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35.3 35.3" version="1.1"><title>Check is muted</title><style>.s0{fill:#191919;}</style><g transform="translate(0,-261.72223)"><path d="m17.6 261.7v35.3L5.3 284.7H0v-10.6l5.3 0zM30.2 273.1l-3.7 3.7-3.7-3.7-2.5 2.5 3.7 3.7-3.7 3.7 2.5 2.5 3.7-3.7 3.7 3.7 2.5-2.5-3.7-3.7 3.7-3.7z" fill="#191919"/></g></svg>`,
"error": `{{ template "header" . }}{{ template "footer" . }}`,
} }
TmplUnhandledGroups = `TODO` TmplUnhandledGroups = `TODO`
Funcs = template.FuncMap{ Funcs = template.FuncMap{
"sub": func(base, amount int) int { return base - amount }, "sub": func(base, amount int) int { return base - amount },
"in": func(t time.Time) time.Duration { return t.Sub(time.Now()).Round(1 * time.Second) }, "in": func(t time.Time) time.Duration { return t.Sub(time.Now()).Round(1 * time.Second) },
"now": func() time.Time { return time.Now() }, "now": func() time.Time { return time.Now() },
"join": func(args []string, c string) string { return strings.Join(args, c) },
} }
) )