Skip to content

Commit ba07768

Browse files
authored
feat: add healthchecks.io service (#42)
1 parent 88d4b7c commit ba07768

File tree

10 files changed

+474
-3
lines changed

10 files changed

+474
-3
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,36 @@ It uses zerolog levels (from highest to lowest):
8787
* trace (zerolog.TraceLevel, -1)
8888

8989
### Modules
90+
#### Healthchecks
91+
Uses the [Healthchecks.io](https://healthchecks.io) service to check whether `discord-bot` is online or not.
92+
It can triggers alerts on several systems if it is down.
93+
94+
**If you don't want to use it, leave `uuid` empty.**
95+
96+
You can use your own version by using a custom `base_url`.
97+
98+
JSON configuration used:
99+
```json
100+
"healthchecks": {
101+
"base_url": "https://hc-ping.com/",
102+
"uuid": "00000000-0000-0000-0000-000000000000",
103+
"started_message": "discord-bot started",
104+
"failed_message": "discord-bot failed"
105+
}
106+
```
107+
108+
| JSON Parameter | Mandatory | Type | Default value | Description |
109+
| --------------- | --------- | ------ | -------------------- | ----------------------------------------------------------------- |
110+
| base_url | NO | string | https://hc-ping.com/ | url to ping, by default use the healthchecks service |
111+
| uuid | YES | string | | uuid, on healthchecks dashboard it's after `https://hc-ping.com/` |
112+
| started_message | NO | string | discord-bot started | message sent to healthchecks when discord-bot starts |
113+
| failed_message | NO | string | discord-bot failed | message sent to healthchecks when discord-bot stops |
114+
115+
##### How it works?
116+
Each time you start `discord-bot`, the healthchecks module will check the configuration in `config.json`.
117+
Then, when all modules have been started, it sends a `Start` ping message to indicate that the discord-bot is up and running.
118+
Finally, if `discord-bot` receives a signal from the OS to terminate the program, it will send a `Fail` ping message.
119+
90120
#### Welcome
91121
Define the user's role when using an emoji.
92122
You can define one or more messages in only one channel.

config.template.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
"purge_below_count_members_not_in_guild": 10
2424
}
2525
]
26+
},
27+
"healthchecks": {
28+
"base_url": "",
29+
"uuid": "",
30+
"started_message": "",
31+
"failed_message": ""
2632
}
2733
}
2834
}

configuration/configuration.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strconv"
1212
"strings"
1313

14+
"github.com/blueprintue/discord-bot/healthchecks"
1415
"github.com/blueprintue/discord-bot/welcome"
1516
)
1617

@@ -42,7 +43,8 @@ type Log struct {
4243
}
4344

4445
type Modules struct {
45-
WelcomeConfiguration welcome.Configuration `json:"welcome"`
46+
WelcomeConfiguration welcome.Configuration `json:"welcome"`
47+
HealthcheckConfiguration healthchecks.Configuration `json:"healthchecks"`
4648
}
4749

4850
// ReadConfiguration read `config.json` file and update values with env if found.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.22
44

55
require (
66
github.com/bwmarrin/discordgo v0.27.1
7+
github.com/crazy-max/gohealthchecks v0.4.1
78
github.com/ilya1st/rotatewriter v0.0.0-20171126183947-3df0c1a3ed6d
89
github.com/rs/zerolog v1.32.0
910
github.com/stretchr/testify v1.8.4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
22
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
33
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4+
github.com/crazy-max/gohealthchecks v0.4.1 h1:gbjZzF/GxwDyP78u37B2/c2iQfq8BEjAHS3eBLM6FcQ=
5+
github.com/crazy-max/gohealthchecks v0.4.1/go.mod h1:gkT8QSdEXZJahyswdTGDbd+q20fWm0DmWW7TWBNtgJg=
46
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
57
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
68
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=

healthchecks/healthcheck.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package healthchecks
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"strings"
8+
9+
"github.com/crazy-max/gohealthchecks"
10+
"github.com/rs/zerolog/log"
11+
)
12+
13+
type Configuration struct {
14+
BaseURL string `json:"base_url"`
15+
UUID string `json:"uuid"`
16+
StartedMessage string `json:"started_message"`
17+
FailedMessage string `json:"failed_message"`
18+
}
19+
20+
type Manager struct {
21+
client *gohealthchecks.Client
22+
baseURL *url.URL
23+
uuid string
24+
startedMessage string
25+
failedMessage string
26+
}
27+
28+
func NewHealthchecksManager(
29+
config Configuration,
30+
) *Manager {
31+
manager := &Manager{}
32+
33+
log.Info().Msg("Checking configuration for Healthchecks")
34+
35+
if !manager.hasValidConfigurationInFile(config) {
36+
return nil
37+
}
38+
39+
return manager
40+
}
41+
42+
func (m *Manager) hasValidConfigurationInFile(config Configuration) bool {
43+
baseRawURL := config.BaseURL
44+
if baseRawURL == "" {
45+
log.Info().
46+
Msg("BaseURL is empty, use default URL https://hc-ping.com/")
47+
48+
baseRawURL = "https://hc-ping.com/"
49+
}
50+
51+
baseURL, err := url.Parse(baseRawURL)
52+
if err != nil {
53+
log.Error().
54+
Err(err).
55+
Str("base_url", baseRawURL).
56+
Msg("BaseURL is invalid")
57+
58+
return false
59+
}
60+
61+
if !strings.HasSuffix(baseURL.Path, "/") {
62+
baseURL.Path += "/"
63+
}
64+
65+
m.baseURL = baseURL
66+
67+
if config.UUID == "" {
68+
log.Error().
69+
Msg("UUID is empty")
70+
71+
return false
72+
}
73+
74+
m.uuid = config.UUID
75+
76+
m.startedMessage = config.StartedMessage
77+
if m.startedMessage == "" {
78+
log.Info().
79+
Msg(`StartedMessage is empty, use default "discord-bot started"`)
80+
81+
m.startedMessage = "discord-bot started"
82+
}
83+
84+
m.failedMessage = config.FailedMessage
85+
if m.failedMessage == "" {
86+
log.Info().
87+
Msg(`FailedMessage is empty, use default "discord-bot stopped"`)
88+
89+
m.failedMessage = "discord-bot stopped"
90+
}
91+
92+
return true
93+
}
94+
95+
func (m *Manager) Run() error {
96+
m.client = gohealthchecks.NewClient(
97+
&gohealthchecks.ClientOptions{
98+
BaseURL: m.baseURL,
99+
},
100+
)
101+
102+
err := m.client.Start(
103+
context.Background(),
104+
gohealthchecks.PingingOptions{
105+
UUID: m.uuid,
106+
Logs: m.startedMessage,
107+
},
108+
)
109+
if err != nil {
110+
log.Error().
111+
Err(err).
112+
Msg("Could not send Start HealthChecks client")
113+
114+
return fmt.Errorf("%w", err)
115+
}
116+
117+
return nil
118+
}
119+
120+
func (m *Manager) Fail() {
121+
err := m.client.Fail(
122+
context.Background(),
123+
gohealthchecks.PingingOptions{
124+
UUID: m.uuid,
125+
Logs: m.failedMessage,
126+
},
127+
)
128+
if err != nil {
129+
log.Error().
130+
Err(err).
131+
Msg("Could not send Fail HealthChecks client")
132+
}
133+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//nolint:paralleltest
2+
package healthchecks_test
3+
4+
import (
5+
"bytes"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
12+
"github.com/blueprintue/discord-bot/healthchecks"
13+
"github.com/rs/zerolog"
14+
"github.com/rs/zerolog/log"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestFail(t *testing.T) {
19+
var bufferLogs bytes.Buffer
20+
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()
21+
22+
currentRequestIdx := 0
23+
24+
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
25+
if currentRequestIdx == 0 {
26+
require.Equal(t, "/00000000-0000-0000-0000-000000000000/start", req.RequestURI)
27+
startedMessage, err := io.ReadAll(req.Body)
28+
require.NoError(t, err)
29+
require.Equal(t, "starts", string(startedMessage))
30+
} else {
31+
require.Equal(t, "/00000000-0000-0000-0000-000000000000/fail", req.RequestURI)
32+
failedMessage, err := io.ReadAll(req.Body)
33+
require.NoError(t, err)
34+
require.Equal(t, "stops", string(failedMessage))
35+
}
36+
37+
currentRequestIdx++
38+
39+
res.WriteHeader(http.StatusOK)
40+
}))
41+
defer svr.Close()
42+
43+
healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
44+
BaseURL: svr.URL,
45+
UUID: "00000000-0000-0000-0000-000000000000",
46+
StartedMessage: "starts",
47+
FailedMessage: "stops",
48+
})
49+
require.NotNil(t, healthchecksManager)
50+
51+
err := healthchecksManager.Run()
52+
require.NoError(t, err)
53+
54+
bufferLogs.Reset()
55+
56+
healthchecksManager.Fail()
57+
58+
parts := strings.Split(bufferLogs.String(), "\n")
59+
require.Equal(t, ``, parts[0])
60+
}
61+
62+
func TestFail_Errors(t *testing.T) {
63+
var bufferLogs bytes.Buffer
64+
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()
65+
66+
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) {
67+
res.WriteHeader(http.StatusInternalServerError)
68+
}))
69+
defer svr.Close()
70+
71+
healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
72+
BaseURL: svr.URL,
73+
UUID: "00000000-0000-0000-0000-000000000000",
74+
StartedMessage: "starts",
75+
FailedMessage: "stops",
76+
})
77+
require.NotNil(t, healthchecksManager)
78+
79+
err := healthchecksManager.Run()
80+
require.Error(t, err)
81+
82+
bufferLogs.Reset()
83+
84+
healthchecksManager.Fail()
85+
86+
parts := strings.Split(bufferLogs.String(), "\n")
87+
require.Equal(t, `{"level":"error","error":"HTTP error 500","message":"Could not send Fail HealthChecks client"}`, parts[0])
88+
require.Equal(t, ``, parts[1])
89+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//nolint:paralleltest
2+
package healthchecks_test
3+
4+
import (
5+
"bytes"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"strings"
10+
"testing"
11+
12+
"github.com/blueprintue/discord-bot/healthchecks"
13+
"github.com/rs/zerolog"
14+
"github.com/rs/zerolog/log"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestRun(t *testing.T) {
19+
var bufferLogs bytes.Buffer
20+
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()
21+
22+
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
23+
require.Equal(t, "/00000000-0000-0000-0000-000000000000/start", req.RequestURI)
24+
startedMessage, err := io.ReadAll(req.Body)
25+
require.NoError(t, err)
26+
require.Equal(t, "starts", string(startedMessage))
27+
28+
res.WriteHeader(http.StatusOK)
29+
}))
30+
defer svr.Close()
31+
32+
healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
33+
BaseURL: svr.URL,
34+
UUID: "00000000-0000-0000-0000-000000000000",
35+
StartedMessage: "starts",
36+
FailedMessage: "stops",
37+
})
38+
require.NotNil(t, healthchecksManager)
39+
40+
bufferLogs.Reset()
41+
42+
err := healthchecksManager.Run()
43+
require.NoError(t, err)
44+
45+
parts := strings.Split(bufferLogs.String(), "\n")
46+
require.Equal(t, ``, parts[0])
47+
}
48+
49+
func TestRun_Errors(t *testing.T) {
50+
var bufferLogs bytes.Buffer
51+
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()
52+
53+
svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) {
54+
res.WriteHeader(http.StatusInternalServerError)
55+
}))
56+
defer svr.Close()
57+
58+
healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
59+
BaseURL: svr.URL,
60+
UUID: "00000000-0000-0000-0000-000000000000",
61+
StartedMessage: "starts",
62+
FailedMessage: "stops",
63+
})
64+
require.NotNil(t, healthchecksManager)
65+
66+
bufferLogs.Reset()
67+
68+
err := healthchecksManager.Run()
69+
require.Error(t, err)
70+
71+
parts := strings.Split(bufferLogs.String(), "\n")
72+
require.Equal(t, `{"level":"error","error":"HTTP error 500","message":"Could not send Start HealthChecks client"}`, parts[0])
73+
require.Equal(t, ``, parts[1])
74+
}

0 commit comments

Comments
 (0)