Browse Source

Publish first working public version (1.0)

tags/v1.1
Moritz Marquardt 6 months ago
commit
139a98a3ee
18 changed files with 751 additions and 0 deletions
  1. 4
    0
      .dockerignore
  2. 3
    0
      .gitignore
  3. 16
    0
      Dockerfile
  4. 7
    0
      LICENSE
  5. 63
    0
      README.md
  6. 76
    0
      api.go
  7. 102
    0
      check.go
  8. 17
    0
      chihuahua.example.yml
  9. 83
    0
      cmd/main.go
  10. 90
    0
      notify.go
  11. 45
    0
      print.go
  12. 106
    0
      types/check.go
  13. 32
    0
      types/config.go
  14. 10
    0
      types/server.go
  15. BIN
      web/favicon.ico
  16. 49
    0
      web/index.html
  17. 13
    0
      web/logo.svg
  18. 35
    0
      web/script.js

+ 4
- 0
.dockerignore View File

@@ -0,0 +1,4 @@
/chihuahua
/chihuahua.yml
/web/data.go
/Dockerfile

+ 3
- 0
.gitignore View File

@@ -0,0 +1,3 @@
/chihuahua
/chihuahua.yml
/web/data.go

+ 16
- 0
Dockerfile View File

@@ -0,0 +1,16 @@
FROM golang AS build

ADD . /go/src/codeberg.org/momar/chihuahua
RUN go get codeberg.org/momar/chihuahua/cmd
RUN go get github.com/go-bindata/go-bindata/...
WORKDIR /go/src/codeberg.org/momar/chihuahua
RUN go generate
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static" -s -w' -o /tmp/chihuahua codeberg.org/momar/chihuahua/cmd

FROM alpine
RUN apk add --no-cache openssh ca-certificates && mkdir -p /data/.ssh
COPY --from=build /tmp/chihuahua /bin/chihuahua
EXPOSE 80
ENV HOME /data
ENV ADDRESS :80
CMD ["/bin/chihuahua", "watch", "-c", "/data/chihuahua.yml"]

+ 7
- 0
LICENSE View File

@@ -0,0 +1,7 @@
Copyright 2019 Moritz Marquardt

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 63
- 0
README.md View File

@@ -0,0 +1,63 @@
# chihuahua
**The smallest watchdog on earth.** A [monitoring-plugins](https://www.monitoring-plugins.org/) compatible tiny server status script with a simple web frontend, built with [Go](https://golang.org), [Air](https://github.com/aofei/air), [Vue.js](https://vuejs.org/) and [Bulma](https://bulma.io/).

![Screenshot](https://i.vgy.me/efPuhz.png)

## Getting Started
```
mkdir -p data/.ssh
ssh-keygen -b 2048 -f data/.ssh/id_rsa -P ""
nano data/chihuahua.yml
docker run -d -p 8080:80 -v "$PWD/data:/data" momar/chihuahua
xdg-open http://localhost:8080
```

## Example chihuahua.yml
```yaml
notifications:
- provider: gotify # Send notifications via https://gotify.net/
server: https://gotify.example.org
token: QwErTyUiOpAsDfG
- provider: email # Send notifications via E-Mail
host: smtp.example.org
port: 25
username: example
password: QwErTyUiOpAsDfG
from: noreply@example.org
to: postmaster@example.org

servers:
example:
ssh: monitoring@example.org -p 2222 # SSH connection parameters for the server. Leave away to run checks locally.
checks:
disk-space: check_disk -w 15% -c 5% # monitoring-plugins compatible check command
```

## API
```
GET /checks
GET /checks/:server
GET /checks/:server/:check
```

## Development
Requires [Go](https://golang.org)
```
go get codeberg.org/momar/chihuahua/...
cd $GOPATH/src/codeberg.org/momar/chihuahua
go generate
cp chihuahua.example.yml chihuahua.yml && nano chihuahua.yml
go build -o chihuahua ./cmd
DEBUG=1 ./chihuahua once -c chihuahua.yml
```

### Web interface
Requires [Caddy](https://caddyserver.com)
```
cd web
caddy
```

## Roadmap
- Add custom messages to checks
- More notification providers (mainly [Clockwork SMS](http://clockworksms.com/))

+ 76
- 0
api.go View File

@@ -0,0 +1,76 @@
package chihuahua

//go:generate go-bindata -pkg web -fs -prefix web -o web/data.go -ignore data\\.go web

import (
"net/http"
"os"

"codeberg.org/momar/chihuahua/web"

"codeberg.org/momar/ternary"

"codeberg.org/momar/chihuahua/types"
"codeberg.org/momar/logg"
"github.com/aofei/air"
)

func Api(servers map[string]*types.Server) {

// getCheck returns the check results
getCheck := func(req *air.Request, res *air.Response) error {
result := map[string]map[string]*types.Check{}
for serverName, server := range servers {
result[serverName] = map[string]*types.Check{}
for checkName, check := range server.Checks {
result[serverName][checkName] = check
}
}

s := req.Param("server").Value()
c := req.Param("check").Value()
if c != nil || s != nil {
sres, ok := result[s.String()]
if ok && c != nil {
cres, ok := sres[c.String()]
if ok {
return res.WriteJSON(cres)
}
} else if ok {
return res.WriteJSON(sres)
}
res.Status = 404
return res.WriteJSON(map[string]bool{})
}
return res.WriteJSON(result)
}

// putMessage adds a message to a check
putMessage := func(req *air.Request, res *air.Response) error {
// TODO
return nil
}

// deleteMessage deletes the message from a check
deleteMessage := func(req *air.Request, res *air.Response) error {
// TODO:
return nil
}

app := air.New()
app.GET("/checks", getCheck)
app.GET("/checks/:server", getCheck)
app.GET("/checks/:server/:check", getCheck)
app.PUT("/checks/:server/:check", putMessage)
app.DELETE("/checks/:server/:check", deleteMessage)

app.NotFoundHandler = air.WrapHTTPHandler(http.FileServer(web.AssetFile()))

app.Address = ternary.Default(os.Getenv("ADDRESS"), ":8080").(string)
logg.Tag("server").Info("Listening on %s", app.Address)
err := app.Serve()
if err != nil {
logg.Error("%s", err)
os.Exit(2)
}
}

+ 102
- 0
check.go View File

@@ -0,0 +1,102 @@
package chihuahua

import (
"context"
"os/exec"
"strings"
"time"

"codeberg.org/momar/chihuahua/types"
"codeberg.org/momar/logg"
)

// MaxConnections defines the maximum number of simultaneous connections against a single server
const MaxConnections = 5

const ConnectionTimeout = 30 * time.Second

// RunCheck runs a check and populates it with the interpreted result
func RunCheck(checkName string, check *types.Check, shell []string) {
logg.Tag("check", checkName).Debug("Executing command: %#v", append(shell, check.Command))
ctx, cancel := context.WithTimeout(context.Background(), ConnectionTimeout)
defer cancel()
output, err := exec.CommandContext(ctx, shell[0], append(shell[1:], check.Command)...).Output()
if s, ok := err.(*exec.ExitError); ok {
check.Error = strings.TrimSpace(string(s.Stderr))
}

if err == nil {
check.Status = types.StatusOk
} else if err.Error() == "exit status 1" {
check.Status = types.StatusWarning
} else if err.Error() == "exit status 2" {
check.Status = types.StatusCritical
} else {
check.Status = types.StatusUnknown
if err.Error() != "exit status 3" {
check.Error = strings.TrimSpace(err.Error() + "\n" + check.Error)
}
}

check.Details = strings.SplitN(strings.SplitN(string(output), "\n", 2)[0], "|", 2)[0]

// TODO: parse performance data

check.LastUpdate = time.Now()
logg.Tag("check", checkName).Debug("Check completed, result %d", check.Status)
}

// RunServerChecks runs all checks on a server asynchronously (up to MaxConnections checks at the same time) and populates them with their interpreted results
func RunServerChecks(serverName string, server *types.Server) {
completed := 0
channel := make(chan bool)
connections := 0

if len(server.Shell) <= 0 {
server.Shell = []string{"sh", "-c"}
}

processCheck := func(checkName string, check *types.Check) {
// limit to 5 simultaneous connections
for connections > MaxConnections {
time.Sleep(250 * time.Millisecond)
}
connections++

logg.Tag("check", serverName).Debug("Processing check: %s", checkName)
RunCheck(serverName+"/"+checkName, check, server.Shell)

connections--
completed++
channel <- true
}
for checkName, check := range server.Checks {
go processCheck(checkName, check)
}

// Wait for checks to complete
for completed < len(server.Checks) && <-channel {
logg.Tag("check", serverName).Debug("%d checks left", len(server.Checks)-completed)
}
}

// RunAllChecks runs all checks on all servers asynchronously and populates them with their interpreted results
func RunAllChecks(servers map[string]*types.Server) {
completed := 0
channel := make(chan bool)

processServer := func(serverName string, server *types.Server) {
logg.Tag("check").Debug("Processing server: %s", serverName)
RunServerChecks(serverName, server)
completed++
channel <- true
}
for serverName, server := range servers {
go processServer(serverName, server)
}

// Wait for checks to complete
for completed < len(servers) && <-channel {
logg.Tag("check").Debug("%d servers left", len(servers)-completed)
}
}

+ 17
- 0
chihuahua.example.yml View File

@@ -0,0 +1,17 @@
notifications:
- provider: gotify # Send notifications via https://gotify.net/
server: https://gotify.example.org
token: QwErTyUiOpAsDfG
- provider: email # Send notifications via E-Mail
host: smtp.example.org
port: 25
username: example
password: QwErTyUiOpAsDfG
from: noreply@example.org
to: postmaster@example.org

servers:
example:
ssh: monitoring@example.org -p 2222 # SSH connection parameters for the server. Leave away to run checks locally.
checks:
disk-space: check_disk -w 15% -c 5% # monitoring-plugins compatible check command

+ 83
- 0
cmd/main.go View File

@@ -0,0 +1,83 @@
package main

import (
"io/ioutil"
"os"
"strconv"
"time"

"codeberg.org/momar/ternary"

"codeberg.org/momar/chihuahua"
"codeberg.org/momar/chihuahua/types"

"codeberg.org/momar/logg"
"github.com/teris-io/cli"
"gopkg.in/yaml.v2"
)

func main() {
app := cli.New("the smallest watchdog on earth")

app.WithCommand(cli.NewCommand("watch", "start webserver and watchdog, and repeatedly run monitoring checks (by default every 5 minutes)").
WithOption(cli.NewOption("config", "path to the configuration file").WithChar('c').WithType(cli.TypeString)).
WithOption(cli.NewOption("interval", "check interval in seconds").WithChar('i').WithType(cli.TypeInt)).
WithAction(func(args []string, options map[string]string) int {
cfg := getConfig(options["config"])
servers := cfg.GetServers()

i, _ := strconv.Atoi(options["interval"])
if i <= 0 {
i = 300
}
id := time.Duration(ternary.Default(i, 300).(int)) * time.Second

go chihuahua.Api(servers)
for {
chihuahua.RunAllChecks(servers)
chihuahua.Notify(cfg.Notifications, servers)
logg.Debug("Waiting for next check at %s...", time.Now().Add(id).UTC().Format("2006-01-02 15:04:05"))
time.Sleep(id)
}
return 0
}))

app.WithCommand(cli.NewCommand("once", "run monitoring checks once and print the result (doesn't start the webserver, but will send notifications for everything that's not OK)").
WithOption(cli.NewOption("config", "path to the configuration file").WithChar('c').WithType(cli.TypeString)).
WithAction(func(args []string, options map[string]string) int {
cfg := getConfig(options["config"])
servers := cfg.GetServers()
chihuahua.RunAllChecks(servers)
chihuahua.Notify(cfg.Notifications, servers)
chihuahua.Print(servers)
return 0
}))

/*app.WithCommand(cli.NewCommand("show", "print the results from a remote chihuaua server").
WithArg(cli.NewArg("server", "address of the server").WithType(cli.TypeString)).
WithArg(cli.NewArg("filter", "filter checks by their name using a regular expression").WithType(cli.TypeString).AsOptional()).
WithAction(func(args []string, options map[string]string) int {
// do something
return 0
}))*/

os.Exit(app.Run(os.Args, os.Stdout))
}

func getConfig(cfgPath string) *types.Config {
if cfgPath == "" {
cfgPath = "/etc/chihuahua.yml"
}
cfgFile, err := ioutil.ReadFile(cfgPath)
if err != nil {
logg.Tag("config", "file").Error("%s", err)
os.Exit(2)
}
cfg := &types.Config{}
err = yaml.Unmarshal(cfgFile, cfg)
if err != nil {
logg.Tag("config", "yaml").Error("%s", err)
os.Exit(2)
}
return cfg
}

+ 90
- 0
notify.go View File

@@ -0,0 +1,90 @@
package chihuahua

import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"

"codeberg.org/momar/ternary"

"gopkg.in/gomail.v2"

"codeberg.org/momar/chihuahua/types"
"codeberg.org/momar/logg"
)

var cache = map[string]types.CheckStatus{}

func Notify(settings []map[string]string, servers map[string]*types.Server) {
logg.Tag("notify").Debug("Processing notifications...")
for serverName, server := range servers {
for checkName, check := range server.Checks {
old, ok := cache[serverName+"/"+checkName]
if (!ok && check.Status != types.StatusOk) || check.Status != old {
logg.Tag("notify").Debug("Sending notification for %s", serverName+"/"+checkName)
send(settings, serverName+"/"+checkName, check)
} else {
logg.Tag("notify").Debug("Skipping notification for %s (%s -> %d)", serverName+"/"+checkName, ternary.If(ok, strconv.Itoa(int(old)), "?"), check.Status)
}
cache[serverName+"/"+checkName] = check.Status
}
}
}

func send(settings []map[string]string, checkName string, check *types.Check) {
for i, channel := range settings {
logg.Tag("notify", strconv.Itoa(i)).Debug("Processing channel: %+v", channel)
switch channel["provider"] {

case "gotify":
body, err := json.Marshal(map[string]interface{}{
"title": "[" + check.Status.String() + "] " + checkName + " (previously " + cache[checkName].String() + ")",
"message": check.Details + ternary.If(check.Error != "", "\n**"+check.Error+"**", "").(string),
"priority": 8,
})
if err != nil {
logg.Tag("notify", strconv.Itoa(i), "gotify").Error("Couldn't send notification through Gotify: %s", err)
continue
}

res, err := http.Post(strings.TrimSuffix(channel["server"], "/")+"/message?token="+url.QueryEscape(channel["token"]), "application/json", bytes.NewReader(body))
if err != nil || res.StatusCode != 200 {
data, _ := ioutil.ReadAll(res.Body)
logg.Tag("notify", strconv.Itoa(i), "gotify").Error("Couldn't send notification through Gotify: %s (%s %s)", err, res.Status, data)
continue
}
logg.Tag("notify", strconv.Itoa(i), "gotify").Debug("Notification sent.")

case "email":
d := gomail.NewDialer(
ternary.Default(channel["host"], "127.0.0.1").(string),
ternary.Default(channel["port"], 25).(int),
channel["username"],
channel["password"])
s, err := d.Dial()
if err != nil {
logg.Tag("notify", strconv.Itoa(i), "email").Error("Couldn't send notification through E-Mail: %s", err)
continue
}

m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
m.SetHeader("From", channel["from"])
m.SetHeader("To", channel["to"])
m.SetHeader("Subject", "["+check.Status.String()+"] "+checkName+" (previously "+cache[checkName].String()+")")
m.SetBody("text/plain", check.Details+ternary.If(check.Error != "", "\n\n"+check.Error, "").(string))
if err := gomail.Send(s, m); err != nil {
logg.Tag("notify", strconv.Itoa(i), "email").Error("Couldn't send notification through E-Mail: %s", err)
continue
}
logg.Tag("notify", strconv.Itoa(i), "email").Debug("Notification sent.")

default:
logg.Tag("notify", strconv.Itoa(i)).Error("No such provider: %s", channel["provider"])

}
}
}

+ 45
- 0
print.go View File

@@ -0,0 +1,45 @@
package chihuahua

import (
"fmt"
"strconv"
"strings"

"codeberg.org/momar/chihuahua/types"
)

func Print(servers map[string]*types.Server) {
first := true
for serverName, server := range servers {
if first {
first = false
} else {
fmt.Print("\n")
}
fmt.Printf("\033[1m%s\033[0m\n", serverName)

for checkName, check := range server.Checks {
color := "\033[1;47m"
switch check.Status {
case types.StatusOk:
color = "\033[1;42m"
case types.StatusWarning:
color = "\033[1;43m"
case types.StatusCritical:
color = "\033[1;41m"
}

info := check.Details
if check.Error != "" {
if info == "" {
info = "\033[31m" + check.Error + "\033[0m"
} else {
info += "\n\033[31m" + check.Error + "\033[0m"
}
}
info = strings.ReplaceAll(info, "\n", fmt.Sprintf("\n%"+strconv.Itoa(3+len(checkName))+"s", ""))

fmt.Printf("%s %s \033[0m %s\n", color, checkName, info)
}
}
}

+ 106
- 0
types/check.go View File

@@ -0,0 +1,106 @@
package types

import "time"

// Check describes a command that shall be run in a specific shell, and (if the check has already been run) the result of that command interpreted according to the monitoring-plugins documentation (https://www.monitoring-plugins.org/doc/guidelines.html)
type Check struct {
// Message is the current message for the check
Message string `json:"message,omitempty"`

// Command is the check command line to run inside the shell (e.g. `/usr/lib/monitoring-plugins/check_ping -H 8.8.8.8 -w 100,25% -c 200,50%`)
Command string `json:"command"`

// Status is the result of the check after it has been run
Status CheckStatus `json:"status"`
// Error contains the STDERR output of the check command, and should normally be empty - if it is non-empty, it is very probable that the check couldn't be initiated correctly
Error string `json:"error,omitempty"`
// Details contains the STDOUT output of the check command
Details string `json:"details"`

// Performance contains the performance data parts of the check, mapped to their label
Performance map[string]*CheckPerformance `json:"performance,omitempty"`

// LastUpdate is the last execution date of the check
LastUpdate time.Time `json:"last-update"`
}

// CheckRange describes a range for warning and critical values for a performance data part of a completed check
type CheckRange struct {
// Start is the lower bound of the value (will send an alert if the actual value is smaller), or -Inf if it does not apply
Start float64 `json:"start"`
// End is the upper bound of the value (will send an alert if the actual value is bigger), or Inf if it does not apply
End float64 `json:"end"`
// Inside changes the behaviour (if set to true) to send an alert if the actual value is BIGGER than Start AND SMALLER than End
Inside bool `json:"inside"`
}

// CheckPerformance describes a performance data part of a completed check
type CheckPerformance struct {
// Unit is the unit of measurement (UOM) or the part
Unit CheckUnit `json:"unit"`
// Value is the current value, or NaN if the actual value couldn't be determined (UOM "U" or parsing issues (which additionally cause a warning))
Value float64 `json:"value"`

// Min is the smallest possible value, or NaN if it does not apply or in the case of parsing issues (which additionally cause a warning)
Min float64 `json:"min"`
// Max is the biggest possible value, or NaN if it does not apply or in the case of parsing issues (which additionally cause a warning)
Max float64 `json:"max"`

// Warning is the range definition that will result in a warning alert, or nil if it does not apply or in the case of parsing issues (which additionally cause a warning)
Warning *CheckRange `json:"warning"`
// Critical is the range definition that will result in a critical alert, or nil if it does not apply or in the case of parsing issues (which additionally cause a warning)
Critical *CheckRange `json:"critical"`
}

// CheckStatus describes the result of a check (StatusOk, StatusWarning, StatusCritical, StatusUnknown)
type CheckStatus int

func (s CheckStatus) String() string {
switch s {
case 0:
return "OK"
case 1:
return "WARNING"
case 2:
return "CRITICAL"
default:
return "UNKNOWN"
}
}

// CheckUnit describes the unit of measurement (UOM) for a check value
type CheckUnit string

const (
// StatusOk is the result of a check that returned with the exit code 0
StatusOk CheckStatus = 0
// StatusWarning is the result of a check that returned with the exit code 1
StatusWarning CheckStatus = 1
// StatusCritical is the result of a check that returned with the exit code 2
StatusCritical CheckStatus = 2
// StatusUnknown is the result of a check that returned with a different exit code or threw an error during execution
StatusUnknown CheckStatus = 3

// UnitNumber is the unit used for a number of things (e.g. users, processes, load averages)
UnitNumber CheckUnit = ""
// UnitSeconds is the unit used for an elapsed time in seconds
UnitSeconds CheckUnit = "s"
// UnitMilliseconds is the unit used for an elapsed time in milliseconds
UnitMilliseconds CheckUnit = "ms"
// UnitMicroseconds is the unit used for an elapsed time in microseconds
UnitMicroseconds CheckUnit = "us"
// UnitPercentage is the unit used for a percentage, normally between 0 and 100
UnitPercentage CheckUnit = "%"
// UnitBytes is the unit used for data sizes in bytes
UnitBytes CheckUnit = "B"
// UnitKilobytes is the unit used for data sizes in kilobytes
UnitKilobytes CheckUnit = "KB"
// UnitMegabytes is the unit used for data sizes in megabytes
UnitMegabytes CheckUnit = "MB"
// UnitGigabytes is the unit used for data sizes in gigabytes
UnitGigabytes CheckUnit = "GB"
// UnitTerabytes is the unit used for data sizes in terabytes
UnitTerabytes CheckUnit = "TB"
// UnitCounter is the unit used for a continuous counter (such as bytes transmitted on an interface)
UnitCounter CheckUnit = "c"
)

+ 32
- 0
types/config.go View File

@@ -0,0 +1,32 @@
package types

import "strings"

type ServerConfig struct {
SSH string `yaml:"ssh"`
Checks map[string]string `yaml:"checks"`
}

type Config struct {
Notifications []map[string]string `yaml:"notifications"`
Servers map[string]ServerConfig `yaml:"servers"`
}

func (c *Config) GetServers() map[string]*Server {
result := map[string]*Server{}
for serverName, server := range c.Servers {
result[serverName] = &Server{
Checks: map[string]*Check{},
Shell: []string{},
}
if server.SSH != "" {
result[serverName].Shell = append([]string{"ssh"}, strings.Split(server.SSH, " ")...)
}
for checkName, check := range server.Checks {
result[serverName].Checks[checkName] = &Check{
Command: check,
}
}
}
return result
}

+ 10
- 0
types/server.go View File

@@ -0,0 +1,10 @@
package types

// Server is a group of checks, and should normally refer to a single physical or virtual machine.
type Server struct {
// Checks is a slice containing the Checks to be run on this server
Checks map[string]*Check

// Shell is the default shell used to run commands on this server, including all arguments except for the command itself (e.g. `[]string{"sh", "-c"}` or `[]string{"ssh", "server.example.org", "sh", "-c"}`)
Shell []string
}

BIN
web/favicon.ico View File


+ 49
- 0
web/index.html View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<title>Server Status</title>
<link rel="shortcut icon" href="favicon.ico">

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
<body>
<main class="section" id="app">
<div class="container">
<h1 class="title" style="display: flex; align-items: center">
<img src="logo.svg"> Server Status
</h1>
</div>
<section class="container" style="margin-top: 2em" v-for="(server, serverName) in servers" :key="serverName">
<h2 class="subtitle" style="margin-bottom: 0.5em"><i class="fa fa-server"></i> <strong>{{ serverName }}</strong></h2>
<div class="columns" style="flex-wrap: wrap">
<article class="panel column is-one-quarter" style="margin-bottom: 0" v-for="(check, checkName) in server" :key="serverName + '/' + checkName">
<p class="panel-heading" style="font-weight: bold" :class="headerByStatus(check.status)">
<i class="fa" :class="iconByStatus(check.status)"></i>
{{ checkName }}
</p>
<div class="panel-block" style="padding: 0" v-if="check.details">
<pre style="white-space: pre-wrap; flex-grow: 1"><code><small>{{ check.details.trim() }}</small></code></pre>
</div>
<div class="panel-block" style="padding: 0" v-if="check.error">
<pre style="white-space: pre-wrap; flex-grow: 1"><code><small class="has-text-danger">{{ check.error.trim() }}</small></code></pre>
</div>
</article>
</div>
</section>
</main>
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong><a href="https://codeberg.org/momar/chihuahua">Chihuahua</a> &bull; the smallest watchdog on earth</strong> by <a href="https://mo-mar.de">Moritz Marquardt</a>. The source code is licensed under <a href="http://opensource.org/licenses/mit-license.php">The MIT License</a>.
</p>
</div>
</footer>

<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="script.js"></script>
</body>
</html>

+ 13
- 0
web/logo.svg View File

@@ -0,0 +1,13 @@
<?xml version="1.0" ?><svg height="48" id="check" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><defs><style>
.vi-primary {
fill: #FF6E6E;
}

.vi-primary, .vi-accent {
fill-rule: evenodd;
}

.vi-accent {
fill: #0C0058;
}
</style></defs><path class="vi-primary" d="M16,32l3,3L37,17l-3-3Z"/><path class="vi-accent" d="M22,32l-3,3-8-8,3-3Z"/></svg>

+ 35
- 0
web/script.js View File

@@ -0,0 +1,35 @@
window.app = new Vue({
el: "#app",
data: {
servers: {},
},
methods: {
headerByStatus(status) {
switch (status) {
case 0: return ["has-background-success", "has-text-white"];
case 1: return ["has-background-warning"];
case 2: return ["has-background-danger", "has-text-white"];
default: return ["has-background-black", "has-text-warning"];
}
},
iconByStatus(status) {
switch (status) {
case 0: return ["fa-check"];
case 1: return ["fa-exclamation-triangle"];
case 2: return ["fa-times-circle"];
default: return [];
}
},
},
});

function update() {
fetch("/checks").then(r => r.json()).then(r => {
Vue.set(app, "servers", r);
setTimeout(update, 1000);
}).catch(err => {
console.error(err);
setTimeout(update, 250);
})
}
update();

Loading…
Cancel
Save