From 90efd78669743ed606ff9d9bd147443cfb0b8a4a Mon Sep 17 00:00:00 2001 From: Ashish D'Souza Date: Mon, 19 Aug 2024 16:36:01 -0500 Subject: [PATCH] Rewrite camera uptime monitor in Golang --- install.yaml | 2 +- uptime-go/Dockerfile | 12 ++++ uptime-go/uptime/config/config.go | 26 +++++++ uptime-go/uptime/config/config.yaml | 9 +++ uptime-go/uptime/go.mod | 5 ++ uptime-go/uptime/go.sum | 3 + uptime-go/uptime/main.go | 17 +++++ uptime-go/uptime/monitor.go | 104 ++++++++++++++++++++++++++++ uptime-go/uptime/notify/notifier.go | 28 ++++++++ uptime-go/uptime/notify/ntfy.go | 58 ++++++++++++++++ uptime-go/uptime/ping/ip_ping.go | 10 +++ uptime-go/uptime/ping/tcp_ping.go | 1 + uptime-go/uptime/ping/video_ping.go | 10 +++ 13 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 uptime-go/Dockerfile create mode 100644 uptime-go/uptime/config/config.go create mode 100644 uptime-go/uptime/config/config.yaml create mode 100644 uptime-go/uptime/go.mod create mode 100644 uptime-go/uptime/go.sum create mode 100644 uptime-go/uptime/main.go create mode 100644 uptime-go/uptime/monitor.go create mode 100644 uptime-go/uptime/notify/notifier.go create mode 100644 uptime-go/uptime/notify/ntfy.go create mode 100644 uptime-go/uptime/ping/ip_ping.go create mode 100644 uptime-go/uptime/ping/tcp_ping.go create mode 100644 uptime-go/uptime/ping/video_ping.go diff --git a/install.yaml b/install.yaml index e24d80f..a53b488 100644 --- a/install.yaml +++ b/install.yaml @@ -119,7 +119,7 @@ register: docker_build_dir - name: Copy Docker build directory ansible.builtin.copy: - src: uptime/ + src: uptime-go/ dest: '{{docker_build_dir.path}}' mode: preserve - name: Build frigate-uptime Docker image diff --git a/uptime-go/Dockerfile b/uptime-go/Dockerfile new file mode 100644 index 0000000..b078edf --- /dev/null +++ b/uptime-go/Dockerfile @@ -0,0 +1,12 @@ +#FROM golang:1.23 +FROM golang:1.23 +WORKDIR /code + +ENTRYPOINT ["./uptime"] +ENV GOEXPERIMENT=loopvar + +RUN apt update --fix-missing +RUN apt install -y iputils-ping ffmpeg + +COPY uptime . +RUN go build . diff --git a/uptime-go/uptime/config/config.go b/uptime-go/uptime/config/config.go new file mode 100644 index 0000000..d4168af --- /dev/null +++ b/uptime-go/uptime/config/config.go @@ -0,0 +1,26 @@ +package config + +import "os" + +import "gopkg.in/yaml.v3" + + +type Config struct { + Cameras map[string]string `yaml:"cameras"` + PingIntervalSeconds int64 `yaml:"ping_interval_s"` + PingTimeoutSeconds int64 `yaml:"ping_timeout_s"` + ConsecutiveDownThreshold int `yaml:"consecutive_down_threshold"` +} + +func ReadConfig(configFilename string) Config { + var data, readErr = os.ReadFile(configFilename) + if readErr != nil { + panic(readErr) + } + + var config Config + if yamlErr := yaml.Unmarshal(data, &config); yamlErr != nil { + panic(yamlErr) + } + return config +} diff --git a/uptime-go/uptime/config/config.yaml b/uptime-go/uptime/config/config.yaml new file mode 100644 index 0000000..e748bba --- /dev/null +++ b/uptime-go/uptime/config/config.yaml @@ -0,0 +1,9 @@ +cameras: + back_door: 'rtsp://frigate:8554/back_door' + doorbell: 'rtsp://frigate:8554/doorbell' + front_door: 'rtsp://frigate:8554/front_door' + garage: 'rtsp://frigate:8554/garage' +ping_interval_s: 60 +ping_timeout_s: 30 +consecutive_down_threshold: 3 + diff --git a/uptime-go/uptime/go.mod b/uptime-go/uptime/go.mod new file mode 100644 index 0000000..747c859 --- /dev/null +++ b/uptime-go/uptime/go.mod @@ -0,0 +1,5 @@ +module frigate/uptime + +go 1.21.12 + +require gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/uptime-go/uptime/go.sum b/uptime-go/uptime/go.sum new file mode 100644 index 0000000..4bc0337 --- /dev/null +++ b/uptime-go/uptime/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/uptime-go/uptime/main.go b/uptime-go/uptime/main.go new file mode 100644 index 0000000..cdc2cb2 --- /dev/null +++ b/uptime-go/uptime/main.go @@ -0,0 +1,17 @@ +package main + +import "time" + +import ( + "frigate/uptime/config" + "frigate/uptime/notify" +) + +func main() { + var uptimeConfig = config.ReadConfig("config/config.yaml") + var pingInterval = time.Duration(uptimeConfig.PingIntervalSeconds * int64(time.Second)) + var pingTimeout = time.Duration(uptimeConfig.PingTimeoutSeconds * int64(time.Second)) + + var cameraMonitor CameraMonitor = NewCameraMonitor(pingInterval, pingTimeout, uptimeConfig.ConsecutiveDownThreshold, notify.NewNtfyNotifier()) + cameraMonitor.Run(uptimeConfig.Cameras) +} diff --git a/uptime-go/uptime/monitor.go b/uptime-go/uptime/monitor.go new file mode 100644 index 0000000..27a48b2 --- /dev/null +++ b/uptime-go/uptime/monitor.go @@ -0,0 +1,104 @@ +package main + +import ( + "log/slog" + "time" +) + +import ( + "frigate/uptime/ping" + "frigate/uptime/notify" +) + + +type CameraMonitor struct { + pingInterval time.Duration + pingTimeout time.Duration + consecutiveDownThreshold int + downtime map[string]int + notify.Notifier +} + +func NewCameraMonitor(pingInterval time.Duration, pingTimeout time.Duration, consecutiveDownThreshold int, notifier notify.Notifier) CameraMonitor { + return CameraMonitor{ + pingInterval: pingInterval, + pingTimeout: pingTimeout, + consecutiveDownThreshold: consecutiveDownThreshold, + downtime: make(map[string]int), + Notifier: notifier, + } + +} + +func (c CameraMonitor) onCameraUp(camera string) { + c.SendNotification(camera, true) + slog.Info(camera, "camera is back online!") +} + +func (c CameraMonitor) onCameraDown(camera string) { + c.SendNotification(camera, false) + slog.Info(camera, "camera is offline!") +} + +func (c CameraMonitor) onCameraPingResult(camera string, online bool) { + if online { + if c.downtime[camera] >= c.consecutiveDownThreshold { + c.onCameraUp(camera) + } + c.downtime[camera] = 0 + } else { + c.downtime[camera] += 1 + if c.downtime[camera] == c.consecutiveDownThreshold { + c.onCameraDown(camera) + } + } +} + +func (c CameraMonitor) Run(cameras map[string]string) { + type pingResult struct { + camera string + online bool + } + + var pingResultChannel = make(chan pingResult, 4) + + for { + var unknownPingResultCameras = make(map[string]bool, len(cameras)) + var startTime = time.Now() + var timeoutChannel = time.After(c.pingTimeout) + + // Start all pings + for camera, host := range cameras { + go func() { + pingResultChannel <- pingResult{ + camera: camera, + online: ping.VideoPing(host), + } + }() + unknownPingResultCameras[camera] = true + } + + timeout: + // Await ping results or timeout + for range cameras { + select { + case cameraPingResult := <-pingResultChannel: + slog.Debug(cameraPingResult.camera, "camera:", cameraPingResult.online) + c.onCameraPingResult(cameraPingResult.camera, cameraPingResult.online) + delete(unknownPingResultCameras, cameraPingResult.camera) // Maintain set of cameras with unknown ping status + case <-timeoutChannel: + slog.Info("Timed out waiting for cameras", unknownPingResultCameras) + break timeout + } + } + + // Handle timed out camera pings + for camera := range unknownPingResultCameras { + c.onCameraPingResult(camera, false) + } + + var sleepDuration = c.pingInterval - time.Since(startTime) + slog.Debug("Sleeping for", sleepDuration) + time.Sleep(sleepDuration) + } +} diff --git a/uptime-go/uptime/notify/notifier.go b/uptime-go/uptime/notify/notifier.go new file mode 100644 index 0000000..6096205 --- /dev/null +++ b/uptime-go/uptime/notify/notifier.go @@ -0,0 +1,28 @@ +package notify + +import ( + "fmt" + "strings" +) + + +type Notifier interface { + SendNotification(camera string, online bool) +} + +func createMessage(camera string, online bool) string { + var msgTemplates = map[bool]string{ + true: "%s camera is back online!", + false: "%s camera is offline!", + } + + var cameraTitle string + for i, s := range strings.Split(camera, "_") { + if i > 0 { + cameraTitle += " " + } + cameraTitle += strings.ToUpper(string(s[0])) + s[1:] + } + + return fmt.Sprintf(msgTemplates[online], cameraTitle) +} diff --git a/uptime-go/uptime/notify/ntfy.go b/uptime-go/uptime/notify/ntfy.go new file mode 100644 index 0000000..ea3e1e8 --- /dev/null +++ b/uptime-go/uptime/notify/ntfy.go @@ -0,0 +1,58 @@ +package notify + +import ( + "fmt" + "bytes" + "encoding/json" + "log/slog" + "net/http" +) + + +type NtfyNotifier struct { + client *http.Client +} + +func NewNtfyNotifier() NtfyNotifier { + return NtfyNotifier{ + client: &http.Client{}, + } +} + +func (n NtfyNotifier) SendNotification(camera string, online bool) { + type ntfyJson struct { + Topic string `json:"topic"` + Title string `json:"title"` + Message string `json:"message"` + Priority int `json:"priority"` + Click string `json:"click"` + Icon string `json:"icon"` + } + + var reqJson, jsonErr = json.Marshal(ntfyJson{ + Topic: "frigate_camera_uptime", + Title: "Frigate", + Message: createMessage(camera, online), + Priority: 3, + Click: fmt.Sprintf("https://frigate.homelab.net/cameras/%s", camera), + Icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/frigate.png", + }) + if jsonErr != nil { + slog.Error("Failed to construct JSON request body", jsonErr) + return + } + + var req, reqErr = http.NewRequest("POST", "https://ntfy.homelab.net", bytes.NewBuffer([]byte(reqJson))) + if reqErr != nil { + slog.Error("Failed to create HTTP request", reqErr) + } + req.Header.Set("Content-Type", "application/json") + + var resp, respErr = n.client.Do(req) + if respErr != nil { + slog.Error("Failed to send HTTP request", respErr) + } + if resp.StatusCode != 200 { + slog.Error("Ntfy notification returned HTTP", resp.Status) + } +} diff --git a/uptime-go/uptime/ping/ip_ping.go b/uptime-go/uptime/ping/ip_ping.go new file mode 100644 index 0000000..23695c0 --- /dev/null +++ b/uptime-go/uptime/ping/ip_ping.go @@ -0,0 +1,10 @@ +package ping + +import "os/exec" + + +func IPPing(host string) bool { + var cmd = exec.Command("ping", "-w", "3", "-c", "1", host) + var err = cmd.Run() + return err == nil +} diff --git a/uptime-go/uptime/ping/tcp_ping.go b/uptime-go/uptime/ping/tcp_ping.go new file mode 100644 index 0000000..1d90f44 --- /dev/null +++ b/uptime-go/uptime/ping/tcp_ping.go @@ -0,0 +1 @@ +package ping diff --git a/uptime-go/uptime/ping/video_ping.go b/uptime-go/uptime/ping/video_ping.go new file mode 100644 index 0000000..92ce6a3 --- /dev/null +++ b/uptime-go/uptime/ping/video_ping.go @@ -0,0 +1,10 @@ +package ping + +import "os/exec" + + +func VideoPing(url string) bool { + var cmd = exec.Command("ffprobe", "-rtsp_transport", "tcp", "-i", url, "-timeout", "10000000") + var err = cmd.Run() + return err == nil +}