├── .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 |
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 |
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 |