aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGibheer <gibheer+git@zero-knowledge.org>2019-01-09 20:17:49 +0100
committerGibheer <gibheer+git@zero-knowledge.org>2019-01-09 20:17:49 +0100
commit96d853fad68565257102fd2b722b01636f131852 (patch)
tree7ebd0c8a20ba4738293f82910d7bfb270e876ee5
parent3b222e06ed63050cb99020d9d74ca0f1e52959d2 (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.
-rw-r--r--cmd/monfront/main.go266
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) },
}
)