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:
parent
3b222e06ed
commit
96d853fad6
@ -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) },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user