├── .github └── workflows │ └── pipeline.yml ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.go ├── example-config.yaml ├── go.mod ├── go.sum ├── main.go ├── monitor ├── monitor.go ├── monitor_test.go ├── status.go └── status_test.go └── notification ├── client.go ├── discord.go ├── mock.go ├── slack.go └── telegram.go /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | logs 3 | config.yaml 4 | goalive.exe 5 | goalive 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Stef2k16 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 | # Goalive 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/Stef2k16/goalive)](https://goreportcard.com/report/github.com/Stef2k16/goalive) 3 | [![Build & Tests](https://github.com/Stef2k16/goalive/actions/workflows/pipeline.yml/badge.svg)](https://github.com/Stef2k16/goalive/actions) 4 | 5 | **Goalive** is a simple tool to monitor health endpoints of your services using modern notification 6 | clients. 7 | 8 | ## Functionality 9 | **Goalive** allows to define a list of custom endpoints that should be polled periodically. These endpoints should be 10 | dedicated health endpoints that return a 2xx HTTP status code if the service is running fine. 11 | In case of problems, i.e. HTTP status codes != 2xx or connection issues, notifications can be send via Discord, 12 | Telegram, or Slack. To avoid messages en masse for a failing endpoint, notifications are only 13 | send for the first detected failure or if a previously failing endpoint has been fixed. 14 | 15 | Alternatively, the most recent status can be requested manually. 16 | - For Discord, send `!status` to the channel with added bot 17 | - For Telegram, send `/status` to the bot 18 | - For Slack, send `/health` to the channel with the added bot 19 | 20 | ## Setup 21 | The setup consists of two steps: 22 | - Set up of a Discord or Telegram bot 23 | - Get **Goalive** and adjust the configuration to your needs 24 | 25 | ### Setup of Discord, Telegram or Slack 26 | To deliver notifications, you need to run your own bot or app. 27 | 28 | #### Discord 29 | For the configuration of **Goalive** with Discord, you need a bot token and a channel ID. The bot token is required to 30 | connect to your Bot. The channel ID determines the channel to deliver notifications to. 31 | 32 | How to create a bot and receive a token is e.g. detailed [here](https://www.writebots.com/discord-bot-token/). 33 | To get the channel ID, simply right-click the channel of choice and copy the ID. 34 | 35 | #### Telegram 36 | For the configuration of **Goalive** with Telegram, your need a bot token and your User ID. 37 | 38 | To set up a new bot and receive a bot token, simply send `\newbot` to _BotFather_. To retrieve your user ID, you can for 39 | example add the _userinfobot_ in Telegram and start it. The bot will then reply with your user ID. 40 | 41 | #### Slack 42 | The current implementation of the Slack notification client relies on 43 | [Socket Mode](https://api.slack.com/apis/connections/socket). It thus requires you to create your own 44 | Slack Application to retrieve both an app token and a bot token. First create an app ([Slack API](https://api.slack.com/apps)). 45 | In your app's settings under _Basic Information_ you can create an _App-Level Token_ which requires `connections:write` as scope. 46 | Additionally, you have to enable _Socket Mode_ in the settings of the same name. 47 | 48 | To retrieve a bot token, go to the _OAuth & Permissions_ tab in the app settings. The required 49 | scopes are `chat:write` and `commands`. 50 | 51 | Finally, install your app to any of your workspaces and add the created bot to a channel of your choice. You can copy 52 | the channel ID in the channel's details. 53 | 54 | If you need more information on how to create a Slack app, checkout, for example, [this](https://api.slack.com/authentication/basics#scopes) 55 | official tutorial. For more information on the different token types, take a look [here](https://api.slack.com/authentication/token-types#bot). 56 | 57 | ### Get Goalive and Adjust the Configuration to Your Needs. 58 | #### Download the Latest Release 59 | You can download a pre-built executable for Windows (goalive.exe) or Linux (goalive) from 60 | the [release page](https://github.com/Stef2k16/goalive/releases). 61 | 62 | #### Build from Source 63 | If you need an executable for a different platform, Goalive can be built from source with 64 | [Go](https://golang.org/dl/) (version >= 1.18). 65 | 66 | To create an executable, clone the repository and run `go build` within the cloned repository. 67 | 68 | #### Create a Configuration 69 | The repo contains an example configuration 70 | `example-config.yaml`. You at least have to add your bot token and channel ID (for Discord) / user ID (for Telegram) 71 | to the configuration. For Slack, you need an app token, a bot token and the channel ID. 72 | 73 | Additionally, you can define the URLs you want to monitor, the polling interval in seconds, and the location of a log 74 | file. 75 | 76 | After your configuration is ready, you can start the monitoring with `./goalive --config path/to/your/config.yaml` 77 | on Linux-based systems or `goalive.exe --config path/to/your/config.yaml` on Windows. 78 | 79 | ## References 80 | The project relies on fantastic API wrappers for Discord, Slack and Telegram: 81 | - [DiscordGo](https://github.com/bwmarrin/discordgo) 82 | - [Slack API in Go](https://github.com/slack-go/slack) 83 | - [Telebot](https://github.com/tucnak/telebot) 84 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "gopkg.in/yaml.v3" 5 | "os" 6 | ) 7 | 8 | // Config is the configuration for a monitor. 9 | type Config struct { 10 | Notification Notification `yaml:"notification"` 11 | URL []string `yaml:"url"` 12 | PollingInterval int `yaml:"pollingInterval"` 13 | LogFile string `yaml:"logFile"` 14 | } 15 | 16 | // Notification holds the configuration for a notification client. 17 | type Notification struct { 18 | Client string `yaml:"client"` 19 | Token string `yaml:"token"` 20 | AppToken string `yaml:"appToken"` 21 | Channel string `yaml:"channel"` 22 | User string `yaml:"user"` 23 | } 24 | 25 | // New returns a decoded Config from a YAML file at the given path. 26 | func New(filepath string) (Config, error) { 27 | file, err := os.Open(filepath) 28 | if err != nil { 29 | return Config{}, err 30 | } 31 | 32 | var config Config 33 | if err := yaml.NewDecoder(file).Decode(&config); err != nil { 34 | return Config{}, err 35 | } 36 | 37 | if err = file.Close(); err != nil { 38 | return Config{}, err 39 | } 40 | 41 | return config, nil 42 | } 43 | -------------------------------------------------------------------------------- /example-config.yaml: -------------------------------------------------------------------------------- 1 | # Sample configuration for Discord 2 | notification: 3 | client: 'discord' 4 | token: 'Replace Me With Your Bot Token' 5 | # Discord messages are directed towards a channel and thus the ID of a channel is required 6 | channel: 'Replace Me With Your Channel ID' 7 | url: ['https://httpstat.us/404', 'https://httpstat.us/200'] 8 | pollingInterval: 300 #in seconds 9 | logFile: 'log.txt' 10 | 11 | ## Sample configuration for Telegram. 12 | #notification: 13 | # client: 'telegram' 14 | # token: 'Replace Me With Your Bot Token' 15 | # # Telegram messages are directed towards a user and thus the ID of the user is required 16 | # user: 'Replace Me With Your User ID' 17 | #url: ['https://httpstat.us/404', 'https://httpstat.us/200'] 18 | #pollingInterval: 300 #in seconds 19 | #logFile: 'log.txt' 20 | 21 | ## Configuration for Slack 22 | #notification: 23 | # client: 'slack' 24 | # token: 'Replace Me With Your Bot Token (Should be of the form xoxb-*)' 25 | # appToken: 'Replace Me With Your App Token (Should be of the form xapp-*)' 26 | # # Slack messages are directed towards a channel and thus the ID of a channel is required 27 | # channel: 'Replace Me With Your Channel ID' 28 | #url: ['https://httpstat.us/404', 'https://httpstat.us/200'] 29 | #pollingInterval: 300 #in seconds 30 | #logFile: 'log.txt' -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Stef2k16/goalive 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.25.0 7 | github.com/slack-go/slack v0.11.0 8 | gopkg.in/telebot.v3 v3.0.0 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | github.com/gorilla/websocket v1.5.0 // indirect 14 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 15 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= 2 | github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 7 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 8 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 9 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 10 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 11 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 12 | github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= 13 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 14 | github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= 15 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 16 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 17 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 18 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 19 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 20 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 21 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 22 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 23 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 24 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 25 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/slack-go/slack v0.11.0 h1:sBBjQz8LY++6eeWhGJNZpRm5jvLRNnWBFZ/cAq58a6k= 29 | github.com/slack-go/slack v0.11.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= 30 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 33 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 34 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 35 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 38 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 39 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= 40 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 43 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 44 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= 52 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 54 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 55 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 56 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 57 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= 63 | gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= 64 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 68 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 69 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/Stef2k16/goalive/config" 7 | "github.com/Stef2k16/goalive/monitor" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | ) 13 | 14 | func main() { 15 | configPath := parseFlags() 16 | conf, err := config.New(configPath) 17 | if err != nil { 18 | log.Fatal("Error initializing configuration: ", err) 19 | } 20 | 21 | m, err := monitor.New(conf) 22 | if err != nil { 23 | log.Fatal("Error creating monitor: ", err) 24 | } 25 | m.Start() 26 | 27 | fmt.Println("Monitoring and Notification Bot are now running. Press CTRL-C to exit.") 28 | 29 | sc := make(chan os.Signal, 1) 30 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 31 | <-sc 32 | if err := m.NotificationClient.Stop(); err != nil { 33 | log.Fatal("Error closing session of the notification client: ", err) 34 | } 35 | } 36 | 37 | // parseFlags parses the command line flags and returns its value. 38 | func parseFlags() string { 39 | configPath := flag.String("config", "", "path to the configuration file") 40 | flag.Parse() 41 | return *configPath 42 | } 43 | -------------------------------------------------------------------------------- /monitor/monitor.go: -------------------------------------------------------------------------------- 1 | // Package monitor implements functionality to poll endpoints and send notifications about the retrieved status. 2 | package monitor 3 | 4 | import ( 5 | "fmt" 6 | "github.com/Stef2k16/goalive/config" 7 | "github.com/Stef2k16/goalive/notification" 8 | "log" 9 | "os" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Monitor provides the functionality to monitor http endpoints. 15 | type Monitor struct { 16 | logger *log.Logger 17 | NotificationClient notification.Client 18 | urls []string 19 | pollingInterval time.Duration 20 | prevStatus map[string]status 21 | prevStatusMutex *sync.Mutex 22 | } 23 | 24 | // New sets up a new monitor to check services. 25 | func New(conf config.Config) (*Monitor, error) { 26 | file, err := os.OpenFile(conf.LogFile, os.O_RDONLY|os.O_CREATE, 0666) 27 | if err != nil { 28 | return nil, err 29 | } 30 | lg := log.New(file, "", log.Ldate|log.Ltime) 31 | 32 | c, err := notification.GetClient(conf.Notification) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | m := &Monitor{ 38 | logger: lg, 39 | NotificationClient: c, 40 | urls: conf.URL, 41 | pollingInterval: time.Duration(conf.PollingInterval) * time.Second, 42 | prevStatus: make(map[string]status), 43 | prevStatusMutex: &sync.Mutex{}, 44 | } 45 | c.AddStatusHandler(m.statusSummary) 46 | if err := c.Start(); err != nil { 47 | return nil, err 48 | } 49 | 50 | return m, err 51 | 52 | } 53 | 54 | // Start begins the monitoring. 55 | func (m *Monitor) Start() { 56 | for _, url := range m.urls { 57 | ticker := time.NewTicker(m.pollingInterval) 58 | go func(url string) { 59 | for { 60 | m.respondToStatus(url) 61 | <-ticker.C 62 | } 63 | }(url) 64 | } 65 | } 66 | 67 | // StatusSummary creates a summary of the currently cached status for all monitored urls. 68 | func (m *Monitor) statusSummary() string { 69 | var summary string 70 | m.prevStatusMutex.Lock() 71 | for _, st := range m.prevStatus { 72 | part1 := fmt.Sprintf("%s polled at %s:\n", st.url, st.timestamp.Format(time.RFC1123)) 73 | readableStatus := "FAILED" 74 | if st.success { 75 | readableStatus = "SUCCEEDED" 76 | } 77 | part2 := fmt.Sprintf("\tRequest %s with Status %d\n\tBody: %s\n\n", readableStatus, st.code, st.body) 78 | summary += part1 + part2 79 | } 80 | m.prevStatusMutex.Unlock() 81 | return summary 82 | } 83 | 84 | // log writes a new entry to the Monitor's log. 85 | func (m *Monitor) log(message string) { 86 | m.logger.Println(message) 87 | } 88 | 89 | // respondToStatus sends notifications over the monitors channel considering the state of the current and the previous status. 90 | func (m *Monitor) respondToStatus(url string) { 91 | st := getStatus(url) 92 | message := st.String() 93 | m.prevStatusMutex.Lock() 94 | prevStatus, ok := m.prevStatus[url] 95 | m.prevStatusMutex.Unlock() 96 | prevStatusFailed := ok && !prevStatus.success 97 | prevStatusSucceeded := ok && prevStatus.success 98 | prevStatusNotificationFailed := ok && prevStatus.notificationFailed 99 | 100 | if st.success && (prevStatusFailed || prevStatusNotificationFailed) { 101 | err := m.NotificationClient.SendNotification(message) 102 | if err != nil { 103 | st.notificationFailed = true 104 | m.log(fmt.Sprintf("Sending SUCCESS notification for %s failed.\n\tError: %v", st.url, err)) 105 | } 106 | 107 | } else if !st.success && (!ok || prevStatusSucceeded || prevStatusNotificationFailed) { 108 | m.log(message) 109 | err := m.NotificationClient.SendNotification(message) 110 | if err != nil { 111 | st.notificationFailed = true 112 | m.log(fmt.Sprintf("Sending FAILURE notification for %s failed.\n\tError: %v", st.url, err)) 113 | } 114 | } 115 | m.prevStatusMutex.Lock() 116 | m.prevStatus[url] = st 117 | m.prevStatusMutex.Unlock() 118 | } 119 | -------------------------------------------------------------------------------- /monitor/monitor_test.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "github.com/Stef2k16/goalive/notification" 5 | "log" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestStatusSummarySucceeded(t *testing.T) { 12 | timestamp := time.Date(2021, time.Month(6), 11, 20, 36, 14, 0, time.UTC) 13 | prevStatus := map[string]status{ 14 | "https://example.com": { 15 | timestamp: timestamp, 16 | success: true, 17 | code: 200, 18 | body: "Health OK", 19 | url: "https://example.com", 20 | notificationFailed: false, 21 | }, 22 | } 23 | m := Monitor{ 24 | logger: log.Default(), 25 | NotificationClient: notification.NewMockClient(), 26 | urls: []string{"https://example.com"}, 27 | pollingInterval: 0, 28 | prevStatus: prevStatus, 29 | prevStatusMutex: &sync.Mutex{}, 30 | } 31 | summary := m.statusSummary() 32 | expectedSummary := "https://example.com polled at Fri, 11 Jun 2021 20:36:14 UTC:\n" 33 | expectedSummary += "\tRequest SUCCEEDED with Status 200\n\tBody: Health OK\n\n" 34 | if !(summary == expectedSummary) { 35 | t.Errorf(`m.statusSummary() 36 | Got: %s 37 | Expected: %s`, summary, expectedSummary) 38 | } 39 | } 40 | 41 | func TestStatusSummaryFailed(t *testing.T) { 42 | timestamp := time.Date(2021, time.Month(6), 11, 20, 36, 14, 0, time.UTC) 43 | prevStatus := map[string]status{ 44 | "https://example.com": { 45 | timestamp: timestamp, 46 | success: false, 47 | code: 404, 48 | body: "Not found", 49 | url: "https://example.com", 50 | notificationFailed: false, 51 | }, 52 | } 53 | m := Monitor{ 54 | logger: log.Default(), 55 | NotificationClient: notification.NewMockClient(), 56 | urls: []string{"https://example.com"}, 57 | pollingInterval: 0, 58 | prevStatus: prevStatus, 59 | prevStatusMutex: &sync.Mutex{}, 60 | } 61 | summary := m.statusSummary() 62 | expectedSummary := "https://example.com polled at Fri, 11 Jun 2021 20:36:14 UTC:\n" 63 | expectedSummary += "\tRequest FAILED with Status 404\n\tBody: Not found\n\n" 64 | if !(summary == expectedSummary) { 65 | t.Errorf(`m.statusSummary() 66 | Got: %s 67 | Expected: %s`, summary, expectedSummary) 68 | } 69 | } 70 | 71 | func TestStatusSummaryNotificationFailed(t *testing.T) { 72 | timestamp := time.Date(2021, time.Month(6), 11, 20, 36, 14, 0, time.UTC) 73 | prevStatus := map[string]status{ 74 | "https://example.com": { 75 | timestamp: timestamp, 76 | success: false, 77 | code: 0, 78 | body: "Sending notification failed", 79 | url: "https://example.com", 80 | notificationFailed: true, 81 | }, 82 | } 83 | m := Monitor{ 84 | logger: log.Default(), 85 | NotificationClient: notification.NewMockClient(), 86 | urls: []string{"https://example.com"}, 87 | pollingInterval: 0, 88 | prevStatus: prevStatus, 89 | prevStatusMutex: &sync.Mutex{}, 90 | } 91 | summary := m.statusSummary() 92 | expectedSummary := "https://example.com polled at Fri, 11 Jun 2021 20:36:14 UTC:\n" 93 | expectedSummary += "\tRequest FAILED with Status 0\n\tBody: Sending notification failed\n\n" 94 | if !(summary == expectedSummary) { 95 | t.Errorf(`m.statusSummary() 96 | Got: %s 97 | Expected: %s`, summary, expectedSummary) 98 | } 99 | } 100 | 101 | func TestRespondToStatusSuccess(t *testing.T) { 102 | url := "https://httpstat.us/200" 103 | prevStatus := map[string]status{ 104 | url: { 105 | timestamp: time.Time{}, 106 | success: true, 107 | code: 200, 108 | body: "Health OK", 109 | url: url, 110 | notificationFailed: false, 111 | }, 112 | } 113 | m := Monitor{ 114 | logger: log.Default(), 115 | NotificationClient: notification.NewMockClient(), 116 | urls: []string{url}, 117 | pollingInterval: 10, 118 | prevStatus: prevStatus, 119 | prevStatusMutex: &sync.Mutex{}, 120 | } 121 | 122 | m.respondToStatus(url) 123 | st := m.prevStatus[url] 124 | st.timestamp = time.Time{} 125 | expected := status{ 126 | timestamp: time.Time{}, 127 | success: true, 128 | code: 200, 129 | body: "200 OK", 130 | url: url, 131 | notificationFailed: false, 132 | } 133 | if st != expected { 134 | t.Errorf(`m.respondToStatus(%s) 135 | Got: %v 136 | Expected: %v`, url, st, expected) 137 | } 138 | } 139 | 140 | func TestRespondToStatusSuccessWithPreviousFailure(t *testing.T) { 141 | url := "https://httpstat.us/200" 142 | prevStatus := map[string]status{ 143 | url: { 144 | timestamp: time.Time{}, 145 | success: false, 146 | code: 404, 147 | body: "Health not OK", 148 | url: url, 149 | notificationFailed: false, 150 | }, 151 | } 152 | m := Monitor{ 153 | logger: log.Default(), 154 | NotificationClient: notification.NewMockClient(), 155 | urls: []string{url}, 156 | pollingInterval: 10, 157 | prevStatus: prevStatus, 158 | prevStatusMutex: &sync.Mutex{}, 159 | } 160 | 161 | m.respondToStatus(url) 162 | st := m.prevStatus[url] 163 | st.timestamp = time.Time{} 164 | expected := status{ 165 | timestamp: time.Time{}, 166 | success: true, 167 | code: 200, 168 | body: "200 OK", 169 | url: url, 170 | notificationFailed: false, 171 | } 172 | if st != expected { 173 | t.Errorf(`m.respondToStatus(%s) 174 | Got: %v 175 | Expected: %v`, url, st, expected) 176 | } 177 | } 178 | 179 | func TestRespondToStatusFailureWithPreviousSuccess(t *testing.T) { 180 | url := "https://httpstat.us/404" 181 | prevStatus := map[string]status{ 182 | url: { 183 | timestamp: time.Time{}, 184 | success: true, 185 | code: 200, 186 | body: "Health OK", 187 | url: url, 188 | notificationFailed: false, 189 | }, 190 | } 191 | m := Monitor{ 192 | logger: log.Default(), 193 | NotificationClient: notification.NewMockClient(), 194 | urls: []string{url}, 195 | pollingInterval: 10, 196 | prevStatus: prevStatus, 197 | prevStatusMutex: &sync.Mutex{}, 198 | } 199 | 200 | m.respondToStatus(url) 201 | st := m.prevStatus[url] 202 | st.timestamp = time.Time{} 203 | expected := status{ 204 | timestamp: time.Time{}, 205 | success: false, 206 | code: 404, 207 | body: "404 Not Found", 208 | url: url, 209 | notificationFailed: false, 210 | } 211 | if st != expected { 212 | t.Errorf(`m.respondToStatus(%s) 213 | Got: %v 214 | Expected: %v`, url, st, expected) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /monitor/status.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // status holds the results and metadata of one request to an endpoint. 11 | type status struct { 12 | timestamp time.Time 13 | success bool 14 | code int 15 | body string 16 | url string 17 | notificationFailed bool 18 | } 19 | 20 | func (st *status) String() string { 21 | if st.success { 22 | return fmt.Sprintf("Request for %v SUCCEEDED at %s\n\tStatus: %d\n\tBody: %s", 23 | st.url, st.timestamp.Format(time.RFC1123), st.code, st.body) 24 | } 25 | return fmt.Sprintf("Request for %s FAILED at %s\n\tStatus: %d\n\tBody: %s", 26 | st.url, st.timestamp.Format(time.RFC1123), st.code, st.body) 27 | } 28 | 29 | // getStatus sends a get request to request the status of a service. 30 | func getStatus(url string) status { 31 | resp, err := http.Get(url) 32 | if err != nil { 33 | return status{ 34 | timestamp: time.Now(), 35 | success: false, 36 | code: 0, 37 | body: err.Error(), 38 | url: url, 39 | notificationFailed: false, 40 | } 41 | } 42 | body, err := io.ReadAll(resp.Body) 43 | if err != nil { 44 | return status{ 45 | timestamp: time.Now(), 46 | success: false, 47 | code: 0, 48 | body: err.Error(), 49 | url: url, 50 | notificationFailed: false, 51 | } 52 | } 53 | successfulResponse := resp.StatusCode >= 200 && resp.StatusCode < 300 54 | st := status{ 55 | timestamp: time.Now(), 56 | success: successfulResponse, 57 | code: resp.StatusCode, 58 | body: string(body), 59 | url: url, 60 | notificationFailed: false, 61 | } 62 | return st 63 | } 64 | -------------------------------------------------------------------------------- /monitor/status_test.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestStatusToStringSuccess(t *testing.T) { 9 | timestamp := time.Date(2021, time.Month(6), 11, 20, 36, 14, 0, time.UTC) 10 | st := status{ 11 | timestamp: timestamp, 12 | success: true, 13 | code: 200, 14 | body: "Health OK", 15 | url: "https://example.com", 16 | notificationFailed: false, 17 | } 18 | stString := st.String() 19 | expected := "Request for https://example.com SUCCEEDED at Fri, 11 Jun 2021 20:36:14 UTC\n\tStatus: 200\n\tBody: Health OK" 20 | if !(stString == expected) { 21 | t.Errorf(`st.String() 22 | Got: %s 23 | Expected: %s`, stString, expected) 24 | } 25 | } 26 | 27 | func TestStatusToStringFailure(t *testing.T) { 28 | timestamp := time.Date(2021, time.Month(6), 11, 20, 36, 14, 0, time.UTC) 29 | st := status{ 30 | timestamp: timestamp, 31 | success: false, 32 | code: 500, 33 | body: "Healthcheck failed", 34 | url: "https://example.com", 35 | notificationFailed: false, 36 | } 37 | stString := st.String() 38 | expected := "Request for https://example.com FAILED at Fri, 11 Jun 2021 20:36:14 UTC\n\tStatus: 500\n\tBody: Healthcheck failed" 39 | if !(stString == expected) { 40 | t.Errorf(`st.String() 41 | Got: %s 42 | Expected: %s`, stString, expected) 43 | } 44 | } 45 | 46 | func TestGetStatusSuccess(t *testing.T) { 47 | url := "https://httpstat.us/200" 48 | st := getStatus(url) 49 | st.timestamp = time.Time{} 50 | expected := status{ 51 | timestamp: time.Time{}, 52 | success: true, 53 | code: 200, 54 | body: "200 OK", 55 | url: url, 56 | notificationFailed: false, 57 | } 58 | if st != expected { 59 | t.Errorf(`getStatus(%s) 60 | Got: %v 61 | Expected: %v`, url, st, expected) 62 | } 63 | } 64 | 65 | func TestGetStatusFailure(t *testing.T) { 66 | url := "https://httpstat.us/404" 67 | st := getStatus(url) 68 | st.timestamp = time.Time{} 69 | expected := status{ 70 | timestamp: time.Time{}, 71 | success: false, 72 | code: 404, 73 | body: "404 Not Found", 74 | url: url, 75 | notificationFailed: false, 76 | } 77 | if st != expected { 78 | t.Errorf(`getStatus(%s) 79 | Got: %v 80 | Expected: %v`, url, st, expected) 81 | } 82 | } 83 | 84 | func TestGetStatusInvalidUrl(t *testing.T) { 85 | url := "NotAUrl" 86 | st := getStatus(url) 87 | st.timestamp = time.Time{} 88 | expected := status{ 89 | timestamp: time.Time{}, 90 | success: false, 91 | code: 0, 92 | body: "Get \"NotAUrl\": unsupported protocol scheme \"\"", 93 | url: url, 94 | notificationFailed: false, 95 | } 96 | if st != expected { 97 | t.Errorf(`getStatus(%s) 98 | Got: %v 99 | Expected: %v`, url, st, expected) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /notification/client.go: -------------------------------------------------------------------------------- 1 | // Package notification implements interfaces to notification clients like Discord. 2 | package notification 3 | 4 | import ( 5 | "fmt" 6 | "github.com/Stef2k16/goalive/config" 7 | ) 8 | 9 | const ( 10 | telegram = "telegram" 11 | discord = "discord" 12 | slack = "slack" 13 | mock = "mock" 14 | ) 15 | 16 | // Client describes an interface that allows to send notification to a service. 17 | type Client interface { 18 | Start() error 19 | Stop() error 20 | AddStatusHandler(func() string) 21 | SendNotification(message string) error 22 | } 23 | 24 | // GetClient returns a client of the specified type. Each available type is defined as a constant. 25 | func GetClient(notification config.Notification) (Client, error) { 26 | switch notification.Client { 27 | case discord: 28 | { 29 | return NewDiscordClient(notification.Token, notification.Channel) 30 | } 31 | case telegram: 32 | { 33 | return NewTelegramClient(notification.Token, notification.User) 34 | } 35 | case slack: 36 | { 37 | return NewSlackClient(notification.Token, notification.AppToken, notification.Channel), nil 38 | } 39 | case mock: 40 | { 41 | return NewMockClient(), nil 42 | } 43 | } 44 | return nil, fmt.Errorf("no client found for client type '%s'", notification.Client) 45 | } 46 | -------------------------------------------------------------------------------- /notification/discord.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | "strings" 6 | ) 7 | 8 | // DiscordClient allows to interact with a Discord channel. 9 | type DiscordClient struct { 10 | Session *discordgo.Session 11 | channelID string 12 | } 13 | 14 | // NewDiscordClient starts and returns a new Discord client using the given token and channel. 15 | func NewDiscordClient(token string, channelID string) (*DiscordClient, error) { 16 | s, err := discordgo.New("Bot " + token) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | dc := DiscordClient{ 22 | Session: s, 23 | channelID: channelID, 24 | } 25 | return &dc, nil 26 | } 27 | 28 | // Start opens the discord session. 29 | func (dc *DiscordClient) Start() error { 30 | return dc.Session.Open() 31 | } 32 | 33 | // Stop closes the discord session. 34 | func (dc *DiscordClient) Stop() error { 35 | return dc.Session.Close() 36 | } 37 | 38 | // SendNotification sends the given message to the channel that is specified in the DiscordClient. 39 | func (dc *DiscordClient) SendNotification(message string) error { 40 | _, err := dc.Session.ChannelMessageSend(dc.channelID, message) 41 | return err 42 | } 43 | 44 | // AddStatusHandler adds a handler to the bot that replies with the current monitoring status. 45 | func (dc *DiscordClient) AddStatusHandler(statusSummary func() string) { 46 | dc.Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { 47 | if m.Author.ID == s.State.User.ID { 48 | return 49 | } 50 | message := strings.TrimSpace(m.Content) 51 | if strings.Compare("!status", message) == 0 { 52 | summary := statusSummary() 53 | _, _ = s.ChannelMessageSend(m.ChannelID, summary) 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /notification/mock.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | // MockClient provides a mock implementation of an notification provider for testing purpose. 4 | type MockClient struct{} 5 | 6 | // NewMockClient returns a new mock client. 7 | func NewMockClient() *MockClient { 8 | return &MockClient{} 9 | } 10 | 11 | // Start opens the mock session. 12 | func (mc *MockClient) Start() error { 13 | return nil 14 | } 15 | 16 | // Stop closes the mock session. 17 | func (mc *MockClient) Stop() error { 18 | return nil 19 | } 20 | 21 | // SendNotification mocks sending of notifications. 22 | func (mc *MockClient) SendNotification(_ string) error { 23 | return nil 24 | } 25 | 26 | // AddStatusHandler mocks adding the status handler. 27 | func (mc *MockClient) AddStatusHandler(_ func() string) {} 28 | -------------------------------------------------------------------------------- /notification/slack.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | slackApi "github.com/slack-go/slack" 5 | "github.com/slack-go/slack/socketmode" 6 | ) 7 | 8 | // SlackClient allows to interact with a Discord channel. 9 | type SlackClient struct { 10 | Client *slackApi.Client 11 | channelID string 12 | socketClient *socketmode.Client 13 | } 14 | 15 | // NewSlackClient starts and returns a new Slack client using the given token and channel. 16 | func NewSlackClient(botToken string, appToken string, channelID string) *SlackClient { 17 | client := slackApi.New(botToken, slackApi.OptionAppLevelToken(appToken)) 18 | socketClient := socketmode.New(client) 19 | return &SlackClient{ 20 | Client: client, 21 | channelID: channelID, 22 | socketClient: socketClient, 23 | } 24 | } 25 | 26 | // Start is not required for slack clients and thus always returns nil. 27 | func (sc *SlackClient) Start() error { 28 | go func() { 29 | _ = sc.socketClient.Run() 30 | }() 31 | return nil 32 | } 33 | 34 | // Stop closes the slack session. 35 | func (sc *SlackClient) Stop() error { 36 | return nil 37 | } 38 | 39 | // SendNotification sends the given message to the channel that is specified in the SlackClient. 40 | func (sc *SlackClient) SendNotification(message string) error { 41 | options := []slackApi.MsgOption{ 42 | slackApi.MsgOptionText(message, false), 43 | } 44 | _, _, err := sc.Client.PostMessage(sc.channelID, options...) 45 | return err 46 | } 47 | 48 | // AddStatusHandler adds a handler to the bot that replies with the current monitoring status. 49 | func (sc *SlackClient) AddStatusHandler(statusSummary func() string) { 50 | go func() { 51 | for evt := range sc.socketClient.Events { 52 | switch evt.Type { 53 | case socketmode.EventTypeSlashCommand: 54 | cmd := evt.Data.(slackApi.SlashCommand) 55 | if cmd.Command == "/health" { 56 | summary := statusSummary() 57 | _ = sc.SendNotification(summary) 58 | } 59 | sc.socketClient.Ack(*evt.Request) 60 | } 61 | } 62 | }() 63 | } 64 | -------------------------------------------------------------------------------- /notification/telegram.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | tele "gopkg.in/telebot.v3" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // TelegramClient allows to interact with a Telegram channel. 10 | type TelegramClient struct { 11 | Bot *tele.Bot 12 | userID string 13 | } 14 | 15 | // NewTelegramClient starts and returns a new Telegram client using the given token. 16 | func NewTelegramClient(token string, userID string) (*TelegramClient, error) { 17 | b, err := tele.NewBot(tele.Settings{ 18 | Token: token, 19 | Poller: &tele.LongPoller{Timeout: 10 * time.Second}, 20 | }) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | tc := TelegramClient{ 26 | Bot: b, 27 | userID: userID, 28 | } 29 | return &tc, nil 30 | } 31 | 32 | // Start starts the Telegram bot. 33 | func (tc *TelegramClient) Start() error { 34 | go tc.Bot.Start() 35 | return nil 36 | } 37 | 38 | // Stop stops the Telegram bot. 39 | func (tc *TelegramClient) Stop() error { 40 | tc.Bot.Stop() 41 | return nil 42 | } 43 | 44 | // SendNotification sends the given message to the channel that is specified in the TelegramClient. 45 | func (tc *TelegramClient) SendNotification(message string) error { 46 | id, err := strconv.ParseInt(tc.userID, 10, 64) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | user := &tele.User{ 52 | ID: id, 53 | } 54 | _, err = tc.Bot.Send(user, message) 55 | return err 56 | } 57 | 58 | // AddStatusHandler adds a handler to the bot that replies with the current monitoring status. 59 | func (tc *TelegramClient) AddStatusHandler(statusSummary func() string) { 60 | tc.Bot.Handle("/status", func(c tele.Context) error { 61 | summary := statusSummary() 62 | return c.Send(summary) 63 | }) 64 | } 65 | --------------------------------------------------------------------------------