diff options
author | Gibheer <gibheer+git@zero-knowledge.org> | 2019-01-09 20:17:49 +0100 |
---|---|---|
committer | Gibheer <gibheer+git@zero-knowledge.org> | 2019-01-09 20:17:49 +0100 |
commit | 96d853fad68565257102fd2b722b01636f131852 (patch) | |
tree | 7ebd0c8a20ba4738293f82910d7bfb270e876ee5 /cmd/monfront | |
parent | 3b222e06ed63050cb99020d9d74ca0f1e52959d2 (diff) |
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.
Diffstat (limited to 'cmd/monfront')
-rw-r--r-- | cmd/monfront/main.go | 266 |
1 files changed, 220 insertions, 46 deletions
diff --git a/cmd/monfront/main.go b/cmd/monfront/main.go index fd3d3ec..a3fd50d 100644 --- a/cmd/monfront/main.go +++ b/cmd/monfront/main.go @@ -29,10 +29,13 @@ type ( } Context struct { - Title string - Mappings map[int]map[int]MapEntry - Checks []check - Groups []group + Title string + CurrentPath string + Error string + Mappings map[int]map[int]MapEntry + Checks []check + CheckDetails *checkDetails + Groups []group } MapEntry struct { @@ -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,32 +647,72 @@ var ( <input name="comment" /> </div> <button type="submit">submit</button> - </nav> - <content> - <table> - <thead><tr><th></th><th>group</th><th>host</th><th>worst state</th></tr></thead> - <tbody> - {{ $current := "" }} - {{ $mapping := .Mappings }} - {{ range .Groups }} - <tr> - <td><input type="checkbox" name="nodes" value="{{ .NodeId }}" /></td> - <td>{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}</td> - <td><a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></td> - <td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td> - </tr> - {{ end }} - </tbody> - </table> - </content> - </form> + </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> + <tbody> + {{ $current := "" }} + {{ $mapping := .Mappings }} + {{ range .Groups }} + <tr> + <td><input type="checkbox" name="nodes" value="{{ .NodeId }}" /></td> + <td>{{ if ne $current .Name }}{{ $current = .Name }}{{ .Name }}{{ end }}</td> + <td><a href="/checks?node_id={{ .NodeId }}">{{ .NodeName }}</a></td> + <td class="state-{{ .MappingId }}-{{ .State }}">{{ (index $mapping .MappingId .State).Title }}</td> + </tr> + {{ end }} + </tbody> + </table> + </content> + {{ 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() }, + "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) }, } ) |