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"`
+}