package main
import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"log/slog"
"os"
"strconv"
"strings"
"sync"
"time"
"git.zero-knowledge.org/gibheer/monzero"
)
var (
configPath = flag.String("config", "moncheck.conf", "path to the config file")
)
type (
Config struct {
DB string `json:"db"`
Timeout string `json:"timeout"`
Wait string `json:"wait"`
Path []string `json:"path"`
Workers int `json:"workers"`
CheckerID int `json:"checker_id"`
Log struct {
Format string `json:"format"`
Level string `json:"level"`
Output string `json:"output"`
} `json:"log"`
}
States []int
)
func main() {
flag.Parse()
raw, err := ioutil.ReadFile(*configPath)
if err != nil {
log.Fatalf("could not read config: %s", err)
}
config := Config{Timeout: "30s", Wait: "30s", Workers: 25}
if err := json.Unmarshal(raw, &config); err != nil {
log.Fatalf("could not parse config: %s", err)
}
logger := parseLogger(config)
if err := os.Setenv("PATH", strings.Join(config.Path, ":")); err != nil {
logger.Error("could not set PATH", "error", err, "configured path", config.Path)
os.Exit(1)
}
waitDuration, err := time.ParseDuration(config.Wait)
if err != nil {
logger.Error("could not parse wait duration", "error", err, "wait duration", config.Wait)
os.Exit(1)
}
timeout, err := time.ParseDuration(config.Timeout)
if err != nil {
logger.Error("could not parse timeout", "error", err, "timeout", config.Timeout)
os.Exit(1)
}
db, err := sql.Open("postgres", config.DB)
if err != nil {
logger.Error("could not open database connection", "error", err)
os.Exit(1)
}
hostname, err := os.Hostname()
if err != nil {
logger.Error("could not resolve hostname", "error", err)
os.Exit(1)
}
checker, err := monzero.NewChecker(monzero.CheckerConfig{
CheckerID: config.CheckerID,
DB: db,
Timeout: timeout,
HostIdentifier: hostname,
Executor: monzero.CheckExec,
Logger: logger,
})
if err != nil {
logger.Error("could not create checker instance", "error", err)
os.Exit(1)
}
for i := 0; i < config.Workers; i++ {
go check(checker, waitDuration, logger)
}
wg := sync.WaitGroup{}
wg.Add(1)
wg.Wait()
}
func check(checker *monzero.Checker, waitDuration time.Duration, logger *slog.Logger) {
for {
if err := checker.Next(); err != nil {
if err != monzero.ErrNoCheck {
logger.Info("check returned error", "error", err)
}
time.Sleep(waitDuration)
}
}
}
func (s *States) Value() (driver.Value, error) {
last := len(*s)
if last == 0 {
return "{}", nil
}
result := strings.Builder{}
_, err := result.WriteString("{")
if err != nil {
return "", fmt.Errorf("could not write to buffer: %s", err)
}
for i, state := range *s {
if _, err := fmt.Fprintf(&result, "%d", state); err != nil {
return "", fmt.Errorf("could not write to buffer: %s", err)
}
if i < last-1 {
if _, err := result.WriteString(","); err != nil {
return "", fmt.Errorf("could not write to buffer: %s", err)
}
}
}
if _, err := result.WriteString("}"); err != nil {
return "", fmt.Errorf("could not write to buffer: %s", err)
}
return result.String(), nil
}
func (s *States) Scan(src interface{}) error {
switch src := src.(type) {
case []byte:
tmp := bytes.Trim(src, "{}")
states := bytes.Split(tmp, []byte(","))
result := make([]int, len(states))
for i, state := range states {
var err error
result[i], err = strconv.Atoi(string(state))
if err != nil {
return fmt.Errorf("could not parse element %s: %s", state, err)
}
}
*s = result
return nil
default:
return fmt.Errorf("could not convert %T to states", src)
}
}
// Append prepends the new state before all others.
func (s *States) Add(state int) {
vals := *s
statePos := 5
if len(vals) < 6 {
statePos = len(vals)
}
*s = append([]int{state}, vals[:statePos]...)
return
}
// ToOK returns true when the state returns from != 0 to 0.
func (s *States) ToOK() bool {
vals := *s
if len(vals) == 0 {
return false
}
if len(vals) <= 1 {
return vals[0] == 0
}
if vals[0] == 0 && vals[1] > 0 {
return true
}
return false
}
// parse the log settings and generate the slog output
func parseLogger(config Config) *slog.Logger {
var output io.Writer
switch config.Log.Output {
case "", "stderr":
output = os.Stderr
case "stdout":
output = os.Stdout
default:
var err error
output, err = os.OpenFile(config.Log.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0640)
if err != nil {
log.Fatalf("could not open log file handler: %s", err)
}
}
var level slog.Level
switch config.Log.Level {
case "debug":
level = slog.LevelDebug
case "", "info":
level = slog.LevelInfo
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
log.Fatalf("unknown log level '%s', only 'debug', 'info', 'warn' and 'error' are supported", config.Log.Level)
}
var handler slog.Handler
switch config.Log.Format {
case "", "text":
handler = slog.NewTextHandler(output, &slog.HandlerOptions{Level: level})
case "json":
handler = slog.NewJSONHandler(output, &slog.HandlerOptions{Level: level})
default:
log.Fatalf("unknown log format '%s', only 'text' and 'json' are supported", config.Log.Format)
}
return slog.New(handler)
}