├── .dockerignore ├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config.dev.yaml ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── static └── stylesheet.css └── templates └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | **/tmp 3 | fly.toml 4 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.21' 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | fly.toml 3 | .DS_Store 4 | .air.toml 5 | config.yaml 6 | /incidents 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.21.3-alpine3.17 4 | 5 | # Set destination for COPY 6 | WORKDIR /app 7 | 8 | # Download Go modules 9 | COPY go.mod go.sum ./ 10 | RUN go mod download 11 | 12 | # Copy the source code. Note the slash at the end, as explained in 13 | # https://docs.docker.com/engine/reference/builder/#copy 14 | COPY *.go ./ 15 | COPY *.yaml ./ 16 | COPY templates/ ./templates/ 17 | COPY static/ ./static/ 18 | 19 | 20 | # Build 21 | RUN CGO_ENABLED=0 GOOS=linux go build -o /incidents 22 | 23 | # Optional: 24 | # To bind to a TCP port, runtime parameters must be supplied to the docker command. 25 | # But we can document in the Dockerfile what ports 26 | # the application is going to listen on by default. 27 | # https://docs.docker.com/engine/reference/builder/#expose 28 | EXPOSE 8080 29 | 30 | # Run 31 | CMD ["/incidents"] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

in⧕idents

2 |

3 | 4 | Last commit 5 | 6 | 7 | License 8 | 9 | 10 | Stars 11 | 12 |

13 | Inxidents is a minimal configuration, open-source incident monitoring software with alerts and dashboard for your HTTP/S services written in Go. 14 |

15 | 16 |

17 | 18 | **Current Features:** 19 | - **Real-time (SSE) Health Dashboard** of your services. Perfect for office screens or similar environments. 20 | - **Slack Alerts** whenever a service goes down 🟥 and recovers 🟩. 21 | - **ACK** feature (acknowledge a down service will stop further notifications and will display corresponding service with black and yellow pattern 🚧) 22 | 23 | - Types of checks: GET/POST, StatusCode, containsString (check if certain text is in the response body) 24 | - Visually see the **frequency** of the healthcheck (the white progressbar animation) 25 | - Small project with **simple configuration**. Easy to hack, deploy and further extend for your needs. 26 | 27 | 28 | **Upcoming features:** 29 | - Private/unique URLs for dashboards 30 | - ... ideas and suggestions are welcome 31 | 32 | # Demo 33 | [Click for Demo Dashboard](https://inxidents.fly.dev/) 34 | 35 | image 36 | 37 | # Installation / Deployment 38 | 1. ```cp config.dev.yaml config.yaml``` 39 | 2. Change config.yaml accordingly and add your services: 40 | Example configuration of one service: 41 | ``` 42 | - name: Google 43 | endpoint: https://www.google.com 44 | frequency: 1m 45 | expectedCode: 200 46 | ``` 47 | - **name**: Name of service, currently it needs to be unique for each service you check. 48 | - **endpoint**: HTTP/S endpoint 49 | - **frequency**: Frequency of the health check, examples: "300ms", "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 50 | - **expectedCode**: This is the expected http status code returned from the endpoint. 51 | - **httpMethod**: OPTIONAL - write POST if you are testing POST http Methods. 52 | - **containsString**: OPTIONAL - Check if given string exists in the response body. Value type, string: "FAQ" 53 | - **disableAlerts**: OPTIONAL - For some services one might want only the dashboard and not alerts, set true to those, default is false. 54 | - **userAgent**: OPTIONAL - Set custom user-agent to requests checking the services. 55 | 56 | 3. To get Slack alerts, add an environmental variable called **SLACK_WEBHOOK_URL** containing the incoming slack webhook url. [More info on it here](https://api.slack.com/messaging/webhooks) 57 | 58 | ## Deploy on fly.io 59 | 1. Install [flytcl](https://fly.io/docs/hands-on/install-flyctl/) 60 | 2. Run ```flyctl launch```(answer no to DB or Volume creations) 61 | 3. Run ```flyctl deploy``` to deploy 62 | 63 | To enable Slack alerts when deploying to fly.io you can add the SLACK_WEBHOOK_URL in the fly.toml file 64 | ``` 65 | [env] 66 | SLACK_WEBHOOK_URL = "YOUR INCOMING SLACK WEBHOOK URL" 67 | ``` 68 | 69 | ## Deploy using Docker 70 | Pull [inxidents image](https://hub.docker.com/r/piqoni/inxidents) from dockerhub: 71 | ``` 72 | docker pull piqoni/inxidents 73 | ``` 74 | 75 | Create a directory anywhere in you system and then put your inxidents [config.yaml](https://github.com/piqoni/inxidents/blob/main/config.dev.yaml) file, for example `MYDIR/config.yaml`. 76 | 77 | Run the container (-e SLACK_WEBHOOK_URL is optional, only if you want alerts): 78 | ``` 79 | docker run \ 80 | -p 8080:8080 \ 81 | -v /PATH/TO/YOUR/MYDIR:/app \ 82 | -e SLACK_WEBHOOK_URL=YOUR_SLACK_WEBHOOK_URL_HERE \ 83 | piqoni/inxidents 84 | ``` 85 | Access the dashboard on http://localhost:8080 86 | 87 | ## Tech comments / Architecture 88 | There is no database by design for the time being (if needed in the future, it will likely be SQLite). Apart from the configuration file everything else happens in-memory. The only persistent data history (downtimes history) can be found on Slack alerts and application log files. 89 | ```mermaid 90 | flowchart TB 91 | subgraph MainThread 92 | Main[read services in config.yaml] 93 | end 94 | 95 | subgraph Always Running Goroutines 96 | Service1[Service 1 Check] 97 | Service2[Service 2 Check] 98 | Service3[Service 3 Check] 99 | SendAlerts[When check fails/recovers] 100 | end 101 | 102 | Main -->|goroutine 1| Service1 103 | Main -->|goroutine 2| Service2 104 | Main -->|goroutine 3| Service3 105 | SendAlerts -->|Alert Message| Slack 106 | subgraph Browser Dashboard 107 | Service1 -->|SSE Stream| EventSource 108 | Service2 -->|SSE Stream| EventSource 109 | Service3 -->|SSE Stream | EventSource 110 | end 111 | ``` 112 | 113 | -------------------------------------------------------------------------------- /config.dev.yaml: -------------------------------------------------------------------------------- 1 | - name: Github 2 | endpoint: https://www.github.com 3 | frequency: 5s 4 | expectedCode: 200 5 | 6 | - name: Google 7 | endpoint: https://www.google.com 8 | frequency: 1m 9 | expectedCode: 200 10 | 11 | - name: My Unstable Service 12 | endpoint: http://localhost:8080/unstable 13 | frequency: 25s 14 | expectedCode: 200 15 | 16 | - name: Always Down Service 17 | endpoint: http://localhost:8080/asdfasdfj 18 | frequency: 20s 19 | expectedCode: 200 20 | 21 | - name: Inxidents 22 | endpoint: https://incidents.fly.dev/ 23 | frequency: 1h45m 24 | expectedCode: 200 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module incidents 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/r3labs/sse/v2 v2.10.0 7 | gopkg.in/yaml.v2 v2.4.0 8 | ) 9 | 10 | require ( 11 | golang.org/x/net v0.17.0 // indirect 12 | gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= 6 | github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 9 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 12 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 13 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= 17 | gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 21 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "log/slog" 11 | "net/http" 12 | "os" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/r3labs/sse/v2" 17 | "gopkg.in/yaml.v2" 18 | ) 19 | 20 | type Service struct { 21 | Name string `yaml:"name" json:"name"` 22 | Endpoint string `yaml:"endpoint"` 23 | Frequency time.Duration `yaml:"frequency"` 24 | ExpectedCode int `yaml:"expectedCode"` 25 | ContainsString string `yaml:"containsString"` 26 | HttpMethod string `yaml:"httpMethod"` 27 | DisableAlerts bool `yaml:"disableAlerts"` 28 | UserAgent string `yaml:"userAgent"` 29 | up *bool 30 | ack bool 31 | } 32 | 33 | var webhookSlackURL string = os.Getenv("SLACK_WEBHOOK_URL") 34 | 35 | func checkService(s Service) (bool, error) { 36 | var req *http.Request 37 | var err error 38 | // Create an HTTP request based on the specified method 39 | switch s.HttpMethod { 40 | case "GET": 41 | req, err = http.NewRequest("GET", s.Endpoint, nil) 42 | case "POST": 43 | req, err = http.NewRequest("POST", s.Endpoint, nil) 44 | default: 45 | // Default to GET if the method is not specified or invalid 46 | req, err = http.NewRequest("GET", s.Endpoint, nil) 47 | } 48 | 49 | if err != nil { 50 | return false, err 51 | } 52 | 53 | // Set the User-Agent header if specified 54 | if s.UserAgent != "" { 55 | req.Header.Set("User-Agent", s.UserAgent) 56 | } 57 | 58 | // Send the HTTP request 59 | resp, err := http.DefaultClient.Do(req) 60 | if err != nil { 61 | return false, err 62 | } 63 | defer resp.Body.Close() 64 | 65 | // If there is no ExpectedCode, default 200 66 | if s.ExpectedCode == 0 { 67 | s.ExpectedCode = 200 68 | } 69 | 70 | // Check the HTTP status code 71 | if resp.StatusCode != s.ExpectedCode { 72 | return false, fmt.Errorf(resp.Status) 73 | } 74 | 75 | // Read the response body 76 | body, err := io.ReadAll(resp.Body) 77 | if err != nil { 78 | return false, err 79 | } 80 | 81 | // Check if the specified string exists in the response content 82 | if s.ContainsString != "" { 83 | if !bytes.Contains(body, []byte(s.ContainsString)) { 84 | return false, fmt.Errorf("response does not contain: %s", s.ContainsString) 85 | } 86 | } 87 | 88 | return true, nil 89 | } 90 | 91 | func sendStream(server *sse.Server, s Service, err error) { 92 | serviceData := map[string]any{ 93 | "name": s.Name, 94 | "endpoint": s.Endpoint, 95 | "frequency": s.Frequency.Seconds(), 96 | "expectedCode": s.ExpectedCode, 97 | "up": s.up, 98 | "ack": s.ack, 99 | "error": "", 100 | } 101 | 102 | // If there's an error, set the error field in the map 103 | if err != nil { 104 | serviceData["error"] = err.Error() 105 | } 106 | 107 | // Serialize the map to JSON 108 | jsonData, err := json.Marshal(serviceData) 109 | if err != nil { 110 | // Handle the error 111 | slog.Error("Error marshaling JSON: %v\n", err) 112 | return 113 | } 114 | 115 | // Publish the JSON data as an SSE event 116 | server.Publish("messages", &sse.Event{ 117 | Data: jsonData, 118 | }) 119 | } 120 | 121 | func handleNotification(s *Service, up bool, err error) { 122 | if s.DisableAlerts { 123 | s.up = &up 124 | return 125 | } 126 | 127 | // Recovering Alert 128 | if up && s.up != nil && !*s.up { 129 | sendSlackNotification(fmt.Sprintf("🟩 *<%s|%s>* returning *%v*", s.Endpoint, s.Name, s.ExpectedCode)) 130 | s.ack = false 131 | } 132 | s.up = &up // update s.up so its used for the recovering alert on next run in case is false 133 | 134 | // Down Alert 135 | if err != nil && !s.ack { 136 | sendSlackNotification(fmt.Sprintf("🟥 *<%s|%s>* returning *%s*", s.Endpoint, s.Name, err.Error())) 137 | } 138 | } 139 | 140 | func sendSlackNotification(message string) { 141 | if webhookSlackURL == "" { 142 | return 143 | } 144 | // disable notifications while developing with an early return TODO 145 | // return 146 | message = strconv.Quote(message) 147 | data := fmt.Sprintf(`{"text":%s}`, message) 148 | // Create a POST request with the JSON data 149 | req, err := http.NewRequest("POST", webhookSlackURL, bytes.NewBuffer([]byte(data))) 150 | if err != nil { 151 | slog.Error("Error creating HTTP request:", err) 152 | return 153 | } 154 | 155 | // Set the Content-Type header to application/json 156 | req.Header.Set("Content-type", "application/json") 157 | 158 | // Perform the HTTP request 159 | client := &http.Client{} 160 | resp, err := client.Do(req) 161 | if err != nil { 162 | slog.Error("Error sending Slack notification: %v", err) 163 | return 164 | } 165 | defer resp.Body.Close() 166 | // Check the response status 167 | if resp.Status == "200 OK" { 168 | slog.Info("Slack notification sent successfully") 169 | } 170 | } 171 | 172 | //go:embed templates/* static/* 173 | var templatesFS embed.FS 174 | 175 | func updateAckStatus(services []*Service, serviceName string, ack bool) { 176 | for _, service := range services { 177 | if service.Name == serviceName { 178 | service.ack = ack 179 | fmt.Println("ack status updated for service:", serviceName) 180 | break 181 | } 182 | } 183 | } 184 | 185 | var services []*Service 186 | 187 | func main() { 188 | // Read the service.yaml file 189 | yamlFile, err := os.ReadFile("config.yaml") 190 | if err != nil { 191 | log.Fatalf("Error reading YAML file: %v", err) 192 | } 193 | 194 | // Unmarshal the YAML data into the services slice 195 | if err := yaml.Unmarshal(yamlFile, &services); err != nil { 196 | sendSlackNotification("❌ Error reading the config.yaml file, inxidents will exit and no services will be monitored. Please correct config.yaml and restart the app.") 197 | log.Fatalf("Error unmarshaling YAML: %v", err) 198 | } 199 | 200 | server := sse.New() 201 | server.AutoReplay = false 202 | server.CreateStream("messages") 203 | 204 | // Create a new Mux and set the handler 205 | http.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) { 206 | server.ServeHTTP(w, r) 207 | }) 208 | 209 | http.HandleFunc("/ack", func(w http.ResponseWriter, r *http.Request) { 210 | // Check if the request method is POST 211 | if r.Method != http.MethodPost { 212 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 213 | return 214 | } 215 | 216 | // Decode the JSON payload 217 | var requestBody Service 218 | decoder := json.NewDecoder(r.Body) 219 | err := decoder.Decode(&requestBody) 220 | if err != nil { 221 | http.Error(w, "Invalid request body", http.StatusBadRequest) 222 | return 223 | } 224 | 225 | updateAckStatus(services, requestBody.Name, true) 226 | 227 | w.Header().Set("Content-Type", "application/json") 228 | w.WriteHeader(http.StatusOK) 229 | fmt.Fprintf(w, `{"message": "Request received successfully"}`) 230 | }) 231 | 232 | http.HandleFunc("/up", func(w http.ResponseWriter, r *http.Request) { 233 | w.WriteHeader(http.StatusOK) 234 | }) 235 | 236 | // add an /unstable endpoint that returns 200 and 503 - for testing 237 | http.HandleFunc("/unstable", func(w http.ResponseWriter, r *http.Request) { 238 | // Return with 200 OK if the current time seconds are odd 239 | if time.Now().Unix()%2 == 1 { 240 | w.WriteHeader(http.StatusOK) 241 | } else { 242 | w.WriteHeader(http.StatusServiceUnavailable) 243 | } 244 | }) 245 | 246 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 247 | if r.URL.Path != "/" { 248 | http.NotFound(w, r) 249 | return 250 | } 251 | 252 | // Serve the index.html file from the embedded file system 253 | content, err := templatesFS.ReadFile("templates/index.html") 254 | if err != nil { 255 | http.Error(w, "Unable to read index.html", http.StatusInternalServerError) 256 | return 257 | } 258 | 259 | w.Write(content) 260 | }) 261 | 262 | for _, service := range services { 263 | go func(s *Service) { 264 | for { 265 | up, err := checkService(*s) 266 | handleNotification(s, up, err) 267 | sendStream(server, *s, err) 268 | time.Sleep(s.Frequency) 269 | } 270 | }(service) 271 | } 272 | 273 | http.Handle("/static/", http.FileServer(http.FS(templatesFS))) 274 | http.ListenAndServe("0.0.0.0:8080", nil) 275 | 276 | } 277 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/r3labs/sse/v2" 10 | ) 11 | 12 | // TestCheckURLResponse tests the checkURLResponse function. 13 | func TestCheckURLResponse(t *testing.T) { 14 | // Start a simple HTTP server for testing purposes 15 | go func() { 16 | http.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(http.StatusOK) 18 | }) 19 | http.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) { 20 | w.WriteHeader(http.StatusInternalServerError) 21 | }) 22 | http.ListenAndServe(":8081", nil) 23 | }() 24 | time.Sleep(1 * time.Second) // Wait for the server to start 25 | 26 | okService := Service{ 27 | Name: "OKService", 28 | Endpoint: "http://localhost:8081/ok", 29 | ExpectedCode: http.StatusOK, 30 | } 31 | // Test the function with a URL that returns 200 OK 32 | urlOK := "http://localhost:8081/ok" 33 | resultOK, err := checkService(okService) 34 | if err != nil || !resultOK { 35 | t.Errorf("Expected checkService to return true and no error for URL %s, but got %v and %v", urlOK, resultOK, err) 36 | } 37 | 38 | errorService := Service{ 39 | Name: "ErrorService", 40 | Endpoint: "http://localhost:8081/error", 41 | ExpectedCode: http.StatusOK, 42 | } 43 | // Test the function with a URL that returns an error status code 44 | urlError := "http://localhost:8081/error" 45 | resultError, err := checkService(errorService) 46 | if err == nil || resultError { 47 | t.Errorf("Expected checkService to return false and an error for URL %s, but got %v and %v", urlError, resultError, err) 48 | } 49 | } 50 | 51 | // TestSendStream tests the sendStream function. 52 | func TestSendStream(t *testing.T) { 53 | server := sse.New() 54 | server.CreateStream("messages") 55 | 56 | testService := Service{ 57 | Name: "TestService", 58 | Endpoint: "http://localhost:8081/test", 59 | Frequency: 1 * time.Second, 60 | ExpectedCode: http.StatusOK, 61 | } 62 | 63 | // Test the sendStream function with a test service and no error 64 | err := fmt.Errorf("No error") 65 | sendStream(server, testService, err) 66 | 67 | // Test the sendStream function with a test service and an error 68 | err = fmt.Errorf("Test error") 69 | sendStream(server, testService, err) 70 | 71 | // TODO 72 | } 73 | -------------------------------------------------------------------------------- /static/stylesheet.css: -------------------------------------------------------------------------------- 1 | html * { 2 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | } 4 | 5 | body { 6 | background-color: #e4efe9; 7 | background: 8 | linear-gradient(-90deg, rgba(0, 0, 0, .05) 1px, transparent 1px), 9 | linear-gradient(rgba(0, 0, 0, .05) 1px, transparent 1px), 10 | linear-gradient(-90deg, rgba(0, 0, 0, .04) 1px, transparent 1px), 11 | linear-gradient(rgba(0, 0, 0, .04) 1px, transparent 1px), 12 | linear-gradient(transparent 3px, #f2f2f2 3px, #f2f2f2 78px, transparent 78px), 13 | linear-gradient(-90deg, #aaa 1px, transparent 1px), 14 | linear-gradient(-90deg, transparent 3px, #f2f2f2 3px, #f2f2f2 78px, transparent 78px), 15 | linear-gradient(#aaa 1px, transparent 1px), 16 | #f2f2f2; 17 | background-size: 18 | 4px 4px, 19 | 4px 4px, 20 | 80px 80px, 21 | 80px 80px, 22 | 80px 80px, 23 | 80px 80px, 24 | 80px 80px, 25 | 80px 80px; 26 | } 27 | 28 | .grid-container { 29 | display: grid; 30 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 31 | gap: 10px; 32 | } 33 | 34 | .red { 35 | background: #F52549; 36 | } 37 | 38 | .green { 39 | background: #00CC99; 40 | } 41 | 42 | .acked { 43 | background-color: orange; 44 | background-image: repeating-linear-gradient(120deg, transparent, transparent 10px, rgba(0, 0, 0, 1) 10px, rgba(0, 0, 0, 1) 20px); 45 | } 46 | 47 | .rectangle { 48 | margin: 2px; 49 | width: 100%; 50 | height: 200px; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | text-align: center; 55 | border-radius: 5px; 56 | box-shadow: 5px 5px lightgray; 57 | position: relative; 58 | } 59 | 60 | .rectangle span { 61 | background-color: rgba(255, 255, 255, 0.7); 62 | /* Adjust the alpha value for transparency */ 63 | padding: 4px; 64 | /* Add padding to make the background visible */ 65 | border-radius: 2px; 66 | } 67 | 68 | .progress-bar { 69 | position: absolute; 70 | bottom: 0; 71 | left: 0; 72 | width: 0; 73 | height: 3px; 74 | background-color: white; 75 | border-bottom: 1px; 76 | animation: progressAnimation linear infinite; 77 | animation-fill-mode: forwards; 78 | } 79 | 80 | @keyframes progressAnimation { 81 | 0% { 82 | width: 0; 83 | } 84 | 85 | 100% { 86 | width: 100%; 87 | } 88 | } 89 | 90 | .brand { 91 | text-align: center; 92 | } 93 | 94 | .button-container { 95 | position: absolute; 96 | bottom: 10px; 97 | /* Adjust as needed */ 98 | right: 10px; 99 | /* Adjust as needed */ 100 | z-index: 1; 101 | /* Set a higher value if needed */ 102 | 103 | } 104 | 105 | .transparent-button { 106 | border: 1px solid white; 107 | /* background: transparent; */ 108 | /* color: white; */ 109 | /* Set your desired text color */ 110 | /* padding: 8px 16px; */ 111 | /* Adjust as needed */ 112 | /* border-radius: 3px; */ 113 | /* Adjust as needed */ 114 | } 115 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Inxidents 11 | 12 | 13 | 14 | 15 | 16 |

in⧕idents

17 |
18 |
19 | 20 | 145 | 146 | 147 | 148 | --------------------------------------------------------------------------------