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 {
Title string
CurrentPath string
Error string
Mappings map[int]map[int]MapEntry
Checks []check
CheckDetails *checkDetails
Groups []group
}
@ -54,6 +57,42 @@ type (
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 {
GroupId int
Name string
@ -90,7 +129,10 @@ func main() {
Tmpl = tmpl
http.HandleFunc("/", showChecks)
http.HandleFunc("/static/", showStatic)
http.HandleFunc("/check", showCheck)
http.HandleFunc("/group", showGroup)
http.HandleFunc("/node", showNode)
http.HandleFunc("/checks", showChecks)
http.HandleFunc("/groups", showGroups)
http.HandleFunc("/action", checkAction)
@ -100,6 +142,7 @@ func main() {
}
func checkAction(w http.ResponseWriter, r *http.Request) {
con := Context{}
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("method is not supported"))
@ -157,8 +200,8 @@ func checkAction(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusSeeOther)
return
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "unknown action %s", action)
con.Error = fmt.Sprintf("requested action '%s' does not exist", action[0])
returnError(http.StatusNotFound, con, w)
return
}
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`
where := []string{}
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
params := []interface{}{}
@ -252,6 +295,7 @@ func showChecks(w http.ResponseWriter, r *http.Request) {
Checks: checks,
}
if err := loadMappings(&con); err != nil {
con.Error = "could not load mapping data"
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("problem with the mappings"))
log.Printf("could not load mappings: %s", err)
@ -267,10 +311,6 @@ func showChecks(w http.ResponseWriter, r *http.Request) {
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) {
query := `select groupid, groupname, nodeid, nodename, mapping_id, state
from (
@ -326,6 +366,73 @@ func showGroups(w http.ResponseWriter, r *http.Request) {
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 {
c.Mappings = map[int]map[int]MapEntry{}
rows, err := DB.Query(SQLShowMappings)
@ -356,6 +463,20 @@ func loadMappings(c *Context) error {
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 (
SQLShowMappings = `select mapping_id, target, title, color
from mapping_level`
@ -367,6 +488,7 @@ var (
<html>
<head>
<title>{{ .Title }}</title>
<link rel="shortcut icon" href="/static/favicon" />
<style type="text/css">
* { font-size: 100%; }
body { display: flex; flex-direction: column; padding: 0; margin: 0; }
@ -409,14 +531,26 @@ var (
table tr:hover { background: #dfdfdf; }
table th { background: #cccccc; color: #3a5f78; }
table td, table th { text-align: center; }
table pre { font-size: 75%; }
table code { font-size: 75%; }
table td.disabled { text-decoration: line-through; }
.icon { height: 1em; margin: 0; width: 1em; vertical-align: bottom; margin-right: 0.5em;}
{{ range $mapId, $mapping := .Mappings }}
{{ range $target, $val := $mapping }}
td.state-{{ $mapId }}-{{ $target }} { background: {{ $val.Color }}; }
{{ end }}
{{ end }}
.icon {
display: inline-block;
height: 1em;
margin: 0;
width: 1em;
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>
<script>
setTimeout(function() { if (document.activeElement.tagName == "BODY") { location.reload(true) } }, 30000)
@ -448,7 +582,8 @@ var (
</li>
<li class="submenu"><span class="header">{{ now.Format "2006.01.02 15:04:05" }}</span></li>
</ul>
</nav>`,
</nav>
{{ if .Error }}<div class="error">{{ .Error }}</div>{{ end }}`,
"footer": `</body></html>`,
"checklist": `{{ template "header" . }}
<form method="post" action="/action">
@ -482,9 +617,9 @@ var (
<tr>
<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 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><pre>{{ .Msg }}</pre></td>
<td><code>{{ .Msg }}</code></td>
</tr>
{{ end }}
</tbody>
@ -492,8 +627,7 @@ var (
</content>
</form>
{{ template "footer" . }}`,
"grouplist": `{{ template "header" . }}
<form method="post" action="/action">
"checkformheader": `<form method="post" action="/action">
<section>
<nav>
<div class="option">
@ -513,7 +647,10 @@ var (
<input name="comment" />
</div>
<button type="submit">submit</button>
</nav>
</nav>`,
"checkformfooter": `</section></form>`,
"grouplist": `{{ template "header" . }}
{{ template "checkformheader" . }}
<content>
<table>
<thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead>
@ -531,14 +668,51 @@ var (
</tbody>
</table>
</content>
</form>
{{ template "checkformfooter" . }}
{{ 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`
Funcs = template.FuncMap{
"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) },
"now": func() time.Time { return time.Now() },
"join": func(args []string, c string) string { return strings.Join(args, c) },
}
)