From f23e0bc362de86d941c8350020af53f03d341987 Mon Sep 17 00:00:00 2001 From: kacarmichael Date: Mon, 26 May 2025 01:40:02 -0500 Subject: [PATCH] GPT generated - need to review --- go.mod | 2 ++ healthchecker.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 44 +++++++++++++++++----------- sse.go | 62 +++++++++++++++++++++++++++++++++++++++ types.go | 10 +++++++ 5 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 healthchecker.go create mode 100644 sse.go create mode 100644 types.go diff --git a/go.mod b/go.mod index 23535eb..a6ae1a2 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module HealthCheckAPI + +go 1.23.0 diff --git a/healthchecker.go b/healthchecker.go new file mode 100644 index 0000000..eaa17e6 --- /dev/null +++ b/healthchecker.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "sync" + "time" +) + +var services = []string{ + "https://git.aaronic.cc", + "https://portainer.aaronic.cc", + "https://jellyfin.aaronic.cc", + "https://dnd.aaronic.cc", +} + +type HealthChecker struct { + status map[string]ServiceStatus + mu sync.RWMutex + client *http.Client + sse *SSEBroker +} + +func NewHealthChecker() *HealthChecker { + return &HealthChecker{ + status: make(map[string]ServiceStatus), + client: &http.Client{ + Timeout: 5 * time.Second, + }, + sse: NewSSEBroker(), + } +} + +func (hc *HealthChecker) CheckAll() { + for _, service := range services { + go hc.checkService(service) + } +} + +func (hc *HealthChecker) checkService(url string) { + resp, err := hc.client.Get(url) + status := ServiceStatus{ + URL: url, + CheckedAt: time.Now(), + } + if err != nil { + log.Printf("Error checking %s: %s", url, err) + status.IsUp = false + } else { + defer resp.Body.Close() + status.StatusCode = resp.StatusCode + status.IsUp = resp.StatusCode >= 200 && resp.StatusCode < 300 + log.Printf("%s: %d", url, resp.StatusCode) + } + + hc.mu.Lock() + hc.status[url] = status + hc.mu.Unlock() + + data, _ := json.Marshal(status) + hc.sse.Broadcast(string(data)) +} + +func (hc *HealthChecker) GetStatuses() []ServiceStatus { + hc.mu.RLock() + defer hc.mu.RUnlock() + + results := make([]ServiceStatus, 0, len(hc.status)) + for _, status := range hc.status { + results = append(results, status) + } + + return results +} diff --git a/main.go b/main.go index 91b22d0..4bcaa03 100644 --- a/main.go +++ b/main.go @@ -1,25 +1,35 @@ package main import ( - "fmt" + "encoding/json" + "log" + "net/http" + "time" ) -//TIP To run your code, right-click the code and select Run. Alternatively, click -// the icon in the gutter and select the Run menu item from here. - func main() { - //TIP Press when your caret is at the underlined or highlighted text - // to see how GoLand suggests fixing it. - s := "gopher" - fmt.Println("Hello and welcome, %s!", s) + sseBroker := NewSSEBroker() + checker := NewHealthChecker() - for i := 1; i <= 5; i++ { - //TIP You can try debugging your code. We have set one breakpoint - // for you, but you can always add more by pressing . To start your debugging session, - // right-click your code in the editor and select the Debug option. - fmt.Println("i =", 100/i) - } + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + checker.CheckAll() + <-ticker.C + } + }() + + http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { + statuses := checker.GetStatuses() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(statuses) + }) + + http.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) { + sseBroker.Handler(w, r) + }) + + log.Println("Serving on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) } - -//TIP See GoLand help at jetbrains.com/help/go/. -// Also, you can try interactive lessons for GoLand by selecting 'Help | Learn IDE Features' from the main menu. \ No newline at end of file diff --git a/sse.go b/sse.go new file mode 100644 index 0000000..4d1f506 --- /dev/null +++ b/sse.go @@ -0,0 +1,62 @@ +package main + +import ( + "fmt" + "net/http" + "sync" +) + +type SSEBroker struct { + clients map[chan string]struct{} + lock sync.RWMutex +} + +func NewSSEBroker() *SSEBroker { + return &SSEBroker{ + clients: make(map[chan string]struct{}), + } +} + +func (b *SSEBroker) AddClient(c chan string) { + b.lock.Lock() + defer b.lock.Unlock() + b.clients[c] = struct{}{} +} + +func (b *SSEBroker) RemoveClient(c chan string) { + b.lock.Lock() + defer b.lock.Unlock() + delete(b.clients, c) +} + +func (b *SSEBroker) Broadcast(msg string) { + b.lock.Lock() + defer b.lock.Unlock() + for c := range b.clients { + c <- msg + } +} + +func (b *SSEBroker) Handler(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming not supported!", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + messageChannel := make(chan string) + + b.AddClient(messageChannel) + defer b.RemoveClient(messageChannel) + for { + msg, open := <-messageChannel + if !open { + return + } + fmt.Fprintf(w, "data: %s\n\n", msg) + flusher.Flush() + } +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..69816d8 --- /dev/null +++ b/types.go @@ -0,0 +1,10 @@ +package main + +import "time" + +type ServiceStatus struct { + URL string `json:"url"` + IsUp bool `json:"isUp"` + StatusCode int `json:"statusCode"` + CheckedAt time.Time `json:"checkedAt"` +}