Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add healthchecks.io service #42

Merged
merged 2 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,36 @@ It uses zerolog levels (from highest to lowest):
* trace (zerolog.TraceLevel, -1)

### Modules
#### Healthchecks
Uses the [Healthchecks.io](https://healthchecks.io) service to check whether `discord-bot` is online or not.
It can triggers alerts on several systems if it is down.

**If you don't want to use it, leave `uuid` empty.**

You can use your own version by using a custom `base_url`.

JSON configuration used:
```json
"healthchecks": {
"base_url": "https://hc-ping.com/",
"uuid": "00000000-0000-0000-0000-000000000000",
"started_message": "discord-bot started",
"failed_message": "discord-bot failed"
}
```

| JSON Parameter | Mandatory | Type | Default value | Description |
| --------------- | --------- | ------ | -------------------- | ----------------------------------------------------------------- |
| base_url | NO | string | https://hc-ping.com/ | url to ping, by default use the healthchecks service |
| uuid | YES | string | | uuid, on healthchecks dashboard it's after `https://hc-ping.com/` |
| started_message | NO | string | discord-bot started | message sent to healthchecks when discord-bot starts |
| failed_message | NO | string | discord-bot failed | message sent to healthchecks when discord-bot stops |

##### How it works?
Each time you start `discord-bot`, the healthchecks module will check the configuration in `config.json`.
Then, when all modules have been started, it sends a `Start` ping message to indicate that the discord-bot is up and running.
Finally, if `discord-bot` receives a signal from the OS to terminate the program, it will send a `Fail` ping message.

#### Welcome
Define the user's role when using an emoji.
You can define one or more messages in only one channel.
Expand Down
6 changes: 6 additions & 0 deletions config.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"purge_below_count_members_not_in_guild": 10
}
]
},
"healthchecks": {
"base_url": "",
"uuid": "",
"started_message": "",
"failed_message": ""
}
}
}
4 changes: 3 additions & 1 deletion configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strconv"
"strings"

"github.com/blueprintue/discord-bot/healthchecks"
"github.com/blueprintue/discord-bot/welcome"
)

Expand Down Expand Up @@ -42,7 +43,8 @@ type Log struct {
}

type Modules struct {
WelcomeConfiguration welcome.Configuration `json:"welcome"`
WelcomeConfiguration welcome.Configuration `json:"welcome"`
HealthcheckConfiguration healthchecks.Configuration `json:"healthchecks"`
}

// ReadConfiguration read `config.json` file and update values with env if found.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.22

require (
github.com/bwmarrin/discordgo v0.27.1
github.com/crazy-max/gohealthchecks v0.4.1
github.com/ilya1st/rotatewriter v0.0.0-20171126183947-3df0c1a3ed6d
github.com/rs/zerolog v1.32.0
github.com/stretchr/testify v1.8.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/crazy-max/gohealthchecks v0.4.1 h1:gbjZzF/GxwDyP78u37B2/c2iQfq8BEjAHS3eBLM6FcQ=
github.com/crazy-max/gohealthchecks v0.4.1/go.mod h1:gkT8QSdEXZJahyswdTGDbd+q20fWm0DmWW7TWBNtgJg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
Expand Down
133 changes: 133 additions & 0 deletions healthchecks/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package healthchecks

import (
"context"
"fmt"
"net/url"
"strings"

"github.com/crazy-max/gohealthchecks"
"github.com/rs/zerolog/log"
)

type Configuration struct {
BaseURL string `json:"base_url"`
UUID string `json:"uuid"`
StartedMessage string `json:"started_message"`
FailedMessage string `json:"failed_message"`
}

type Manager struct {
client *gohealthchecks.Client
baseURL *url.URL
uuid string
startedMessage string
failedMessage string
}

func NewHealthchecksManager(
config Configuration,
) *Manager {
manager := &Manager{}

log.Info().Msg("Checking configuration for Healthchecks")

if !manager.hasValidConfigurationInFile(config) {
return nil
}

return manager
}

func (m *Manager) hasValidConfigurationInFile(config Configuration) bool {
baseRawURL := config.BaseURL
if baseRawURL == "" {
log.Info().
Msg("BaseURL is empty, use default URL https://hc-ping.com/")

baseRawURL = "https://hc-ping.com/"
}

baseURL, err := url.Parse(baseRawURL)
if err != nil {
log.Error().
Err(err).
Str("base_url", baseRawURL).
Msg("BaseURL is invalid")

return false
}

if !strings.HasSuffix(baseURL.Path, "/") {
baseURL.Path += "/"
}

m.baseURL = baseURL

if config.UUID == "" {
log.Error().
Msg("UUID is empty")

return false
}

m.uuid = config.UUID

m.startedMessage = config.StartedMessage
if m.startedMessage == "" {
log.Info().
Msg(`StartedMessage is empty, use default "discord-bot started"`)

m.startedMessage = "discord-bot started"
}

m.failedMessage = config.FailedMessage
if m.failedMessage == "" {
log.Info().
Msg(`FailedMessage is empty, use default "discord-bot stopped"`)

m.failedMessage = "discord-bot stopped"
}

return true
}

func (m *Manager) Run() error {
m.client = gohealthchecks.NewClient(
&gohealthchecks.ClientOptions{
BaseURL: m.baseURL,
},
)

err := m.client.Start(
context.Background(),
gohealthchecks.PingingOptions{
UUID: m.uuid,
Logs: m.startedMessage,
},
)
if err != nil {
log.Error().
Err(err).
Msg("Could not send Start HealthChecks client")

return fmt.Errorf("%w", err)
}

return nil
}

func (m *Manager) Fail() {
err := m.client.Fail(
context.Background(),
gohealthchecks.PingingOptions{
UUID: m.uuid,
Logs: m.failedMessage,
},
)
if err != nil {
log.Error().
Err(err).
Msg("Could not send Fail HealthChecks client")
}
}
89 changes: 89 additions & 0 deletions healthchecks/healthchecks_fail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//nolint:paralleltest
package healthchecks_test

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/blueprintue/discord-bot/healthchecks"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
)

func TestFail(t *testing.T) {
var bufferLogs bytes.Buffer
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()

currentRequestIdx := 0

svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
if currentRequestIdx == 0 {
require.Equal(t, "/00000000-0000-0000-0000-000000000000/start", req.RequestURI)
startedMessage, err := io.ReadAll(req.Body)
require.NoError(t, err)
require.Equal(t, "starts", string(startedMessage))
} else {
require.Equal(t, "/00000000-0000-0000-0000-000000000000/fail", req.RequestURI)
failedMessage, err := io.ReadAll(req.Body)
require.NoError(t, err)
require.Equal(t, "stops", string(failedMessage))
}

currentRequestIdx++

res.WriteHeader(http.StatusOK)
}))
defer svr.Close()

healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
BaseURL: svr.URL,
UUID: "00000000-0000-0000-0000-000000000000",
StartedMessage: "starts",
FailedMessage: "stops",
})
require.NotNil(t, healthchecksManager)

err := healthchecksManager.Run()
require.NoError(t, err)

bufferLogs.Reset()

healthchecksManager.Fail()

parts := strings.Split(bufferLogs.String(), "\n")
require.Equal(t, ``, parts[0])
}

func TestFail_Errors(t *testing.T) {
var bufferLogs bytes.Buffer
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()

svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) {
res.WriteHeader(http.StatusInternalServerError)
}))
defer svr.Close()

healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
BaseURL: svr.URL,
UUID: "00000000-0000-0000-0000-000000000000",
StartedMessage: "starts",
FailedMessage: "stops",
})
require.NotNil(t, healthchecksManager)

err := healthchecksManager.Run()
require.Error(t, err)

bufferLogs.Reset()

healthchecksManager.Fail()

parts := strings.Split(bufferLogs.String(), "\n")
require.Equal(t, `{"level":"error","error":"HTTP error 500","message":"Could not send Fail HealthChecks client"}`, parts[0])
require.Equal(t, ``, parts[1])
}
74 changes: 74 additions & 0 deletions healthchecks/healthchecks_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//nolint:paralleltest
package healthchecks_test

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/blueprintue/discord-bot/healthchecks"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
)

func TestRun(t *testing.T) {
var bufferLogs bytes.Buffer
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()

svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
require.Equal(t, "/00000000-0000-0000-0000-000000000000/start", req.RequestURI)
startedMessage, err := io.ReadAll(req.Body)
require.NoError(t, err)
require.Equal(t, "starts", string(startedMessage))

res.WriteHeader(http.StatusOK)
}))
defer svr.Close()

healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
BaseURL: svr.URL,
UUID: "00000000-0000-0000-0000-000000000000",
StartedMessage: "starts",
FailedMessage: "stops",
})
require.NotNil(t, healthchecksManager)

bufferLogs.Reset()

err := healthchecksManager.Run()
require.NoError(t, err)

parts := strings.Split(bufferLogs.String(), "\n")
require.Equal(t, ``, parts[0])
}

func TestRun_Errors(t *testing.T) {
var bufferLogs bytes.Buffer
log.Logger = zerolog.New(&bufferLogs).Level(zerolog.TraceLevel).With().Logger()

svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) {
res.WriteHeader(http.StatusInternalServerError)
}))
defer svr.Close()

healthchecksManager := healthchecks.NewHealthchecksManager(healthchecks.Configuration{
BaseURL: svr.URL,
UUID: "00000000-0000-0000-0000-000000000000",
StartedMessage: "starts",
FailedMessage: "stops",
})
require.NotNil(t, healthchecksManager)

bufferLogs.Reset()

err := healthchecksManager.Run()
require.Error(t, err)

parts := strings.Split(bufferLogs.String(), "\n")
require.Equal(t, `{"level":"error","error":"HTTP error 500","message":"Could not send Start HealthChecks client"}`, parts[0])
require.Equal(t, ``, parts[1])
}
Loading
Loading