Compare commits

..

48 Commits

Author SHA1 Message Date
dependabot[bot]
76111ee133 chore(deps): bump github.com/aws/aws-sdk-go from 1.47.8 to 1.47.9 (#623)
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.47.8 to 1.47.9.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.47.8...v1.47.9)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-13 20:12:39 -05:00
dependabot[bot]
d3a82244a1 chore(deps): bump github.com/aws/aws-sdk-go from 1.45.16 to 1.47.8 (#621)
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.45.16 to 1.47.8.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.45.16...v1.47.8)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-12 17:14:17 -05:00
Bugra Kocabay
807599c05e docs: add instruction to install as binary (#615)
* feat(docs): add install binary command to docs

* feat(docs): change go install command format

* Update README.md

---------

Co-authored-by: Bugra Kocabay <kocabay.bugra@gmail.com>
Co-authored-by: TwiN <twin@linux.com>
2023-11-09 12:21:17 -05:00
Calvin Henderson
de7256e671 feat(alerting): make authentication optional for email provider (#608)
* feat(alerting): Made authentication optional for the email alert provider

* docs: Added  parameter to email alert provider docs

* feat(alerting): Updated email alert to set the LocalName attribute based on the From key

* Updated email provider to disable authentication when no credentials are provided

* Removed `disable-authentication` flag from email provider documentation

* Apply suggestions from code review

---------

Co-authored-by: TwiN <twin@linux.com>
2023-11-04 19:28:06 -04:00
Bugra Kocabay
c6515c4b1c feat(alerting): Add gotify provider (#605)
* feat(alerting): add gotify provider feature

* feat(alerting): update alert message

* feat(test): add tests for gotify provider

* feat(docs): add documentation for gotify provider

* feat(alerting): rename apptoken to token

* feat(docs): update docs for apptoken renaming to token

---------

Co-authored-by: Bugra Kocabay <kocabay.bugra@gmail.com>
Co-authored-by: TwiN <twin@linux.com>
2023-11-03 18:21:52 -04:00
dependabot[bot]
522b958d0f chore(deps): bump github.com/coreos/go-oidc/v3 from 3.6.0 to 3.7.0 (#604)
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 23:06:06 -04:00
dependabot[bot]
16366e169e chore(deps): bump github.com/valyala/fasthttp from 1.49.0 to 1.50.0 (#594)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.49.0 to 1.50.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.49.0...v1.50.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-10-31 20:51:20 -04:00
Bugra Kocabay
ea3ae52f1e Feat/modify discord title (#602)
* feat(alerting): allow discord alert's title to be configurable

* feat(alerting): modify documentation for discord title feature

* feat(test): add tests for discord title modify feature

---------

Co-authored-by: Bugra Kocabay <kocabay.bugra@gmail.com>
Co-authored-by: TwiN <twin@linux.com>
2023-10-31 20:01:54 -04:00
TwiN
5a16151bba ui: Fix issue back button appearing over title when logo is too small 2023-10-31 19:44:54 -04:00
Kevin Richter
802ad7ff8f feat(alerting): Add AWS SES Alerting Provider (#579)
* Add SES Provider

* Formatting

* Rename ses to aws-ses

* Typo

* Parse tag instead of type name

* Use aws.slice to convert string array & rename awsses -> aws-ses

* Rename type

* Update README.md

* Update alerting/config.go

* Rename package aws-ses to awsses

* Update README.md

* PR comments

---------

Co-authored-by: TwiN <twin@linux.com>
2023-10-25 23:52:43 -04:00
dependabot[bot]
619b69f480 chore(deps): bump github.com/wcharczuk/go-chart/v2 from 2.1.0 to 2.1.1 (#591)
Bumps [github.com/wcharczuk/go-chart/v2](https://github.com/wcharczuk/go-chart) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/wcharczuk/go-chart/releases)
- [Commits](https://github.com/wcharczuk/go-chart/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: github.com/wcharczuk/go-chart/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-10-17 19:46:01 -04:00
TwiN
87e029f555 ci: Increase timeout-minutes for test workflow to 10 minutes 2023-10-16 21:45:00 -04:00
TwiN
71c4d3ade1 ui: Use localStorage instead of sessionStorage for refresh interval + collapsed groups
Fixes #509

Supersedes #510
2023-10-16 21:35:43 -04:00
TwiN
315f9b7792 ui(settings): Fix refresh interval padding 2023-10-16 21:33:09 -04:00
dependabot[bot]
bde30b2efb chore(deps): bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0 (#586)
chore(deps): bump github.com/prometheus/client_golang

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 18:38:05 -04:00
dependabot[bot]
88cb92745b chore(deps): bump modernc.org/sqlite from 1.24.0 to 1.26.0 (#585)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.24.0 to 1.26.0.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.24.0...v1.26.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-10-10 21:29:13 -04:00
Heitor
744d63abac fix: Support hexadecimal integers in conditions (#563)
* Fix parsing hexadecimal integers in conditions

Remove the assumption that JSON contains only decimal integers and
instead identify the base from the prefix of the data.

`strconv.ParseInt` can identify the base of the integer based on its
"prefix":
- 0x -> hexadecimal
- 0o -> octal
- 0b -> binary
- decimal otherwise.

* core: add more tests for condiction.evaluate

* fix isEqual parsing integers as strings

* tests: extend conditions

* Test if we can compare equality of hex in JSON

* Test if we can have hex/oct/bin in conditions

---------

Co-authored-by: TwiN <twin@linux.com>
2023-10-04 20:25:18 -04:00
TwiN
c1cdf50851 chore(deps): Bump github.com/TwiN/whois to v1.1.7
Fixes 2bd268670f
2023-10-04 20:20:27 -04:00
TwiN
2bd268670f chore(deps): Bump github.com/TwiN/whois to v1.1.7
relevant: #548
2023-10-04 20:17:37 -04:00
Max Partenfelder
e88bfa8518 fix(alerting): Add support for client.insecure in email alerting provider (#583)
* feat: adding client.insecure flag to email configuration

* chore(review): applying suggested changes

---------

Co-authored-by: TwiN <twin@linux.com>
2023-10-02 20:41:12 -04:00
dependabot[bot]
0fa3c5d114 chore(deps): bump github.com/gofiber/fiber/v2 from 2.46.0 to 2.49.2 (#584)
Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.46.0 to 2.49.2.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.46.0...v2.49.2)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 20:23:16 -04:00
TwiN
0903d28b56 docs: Clean up list of sponsors 2023-09-30 12:06:43 -04:00
dependabot[bot]
b50f3f3646 chore(deps): bump docker/build-push-action from 4 to 5 (#568)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-09-30 08:20:50 -04:00
dependabot[bot]
1b0dfdd09d chore(deps): bump docker/setup-buildx-action from 2 to 3 (#567)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-09-30 08:14:39 -04:00
dependabot[bot]
6d3468d81a chore(deps): bump actions/checkout from 3 to 4 (#560)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-09-30 08:06:35 -04:00
dependabot[bot]
200d007eca chore(deps): bump github.com/miekg/dns from 1.1.55 to 1.1.56 (#578)
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.55 to 1.1.56.
- [Changelog](https://github.com/miekg/dns/blob/master/Makefile.release)
- [Commits](https://github.com/miekg/dns/compare/v1.1.55...v1.1.56)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-09-30 08:03:22 -04:00
wei
24c3a84db9 fix(alerting): add condition results to ntfy (#582) 2023-09-30 07:51:03 -04:00
TwiN
0402bdb774 refactor: Clean up code and improve test coverage 2023-09-28 18:35:18 -04:00
TwiN
c7af44bcb0 fix: Get rid of unnecessary new endpoint 2023-09-28 18:15:14 -04:00
dependabot[bot]
494a8594cc chore(deps): bump docker/setup-qemu-action from 2 to 3 (#569)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-23 17:46:23 -04:00
dependabot[bot]
81dd84e5f2 chore(deps): bump docker/login-action from 2 to 3 (#570)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-23 17:38:54 -04:00
Daniel Hill
aa7f8131cd docs: demonstrate use of environment variables in config (#571)
* docs: demonstrate use of environment variables in config

* chore: reorder env vars to match order used in DB connection
2023-09-23 17:02:50 -04:00
dependabot[bot]
4dea597726 chore(deps): bump golang.org/x/net from 0.11.0 to 0.15.0 (#562)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.11.0 to 0.15.0.
- [Commits](https://github.com/golang/net/compare/v0.11.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-09-23 15:53:18 -04:00
TwiN
8df77b09ed ci: Fix fork conditional (#576)
* ci: Fix fork conditional

* ci: Fix fork conditional

* ci: Fix fork conditional
2023-09-23 15:36:19 -04:00
TwiN
d2b274e609 ci: Tweak concurrency group key (#575)
* ci: Tweak concurrency group key

* ci: Tweak concurrency group key
2023-09-23 15:14:40 -04:00
TwiN
15c81f93d2 ci: Publish only if the workflow is not running on a fork (#574)
* ci: Publish only if the workflow is not running on a fork

Otherwise, the workflow will attempt to publish on forks from contributors that are using the master branch
2023-09-23 15:02:33 -04:00
Henry Barreto
05565e3d0a feat(SSH): Add support for SSH endpoint (#473)
* feat(SSH): Add support for SSH endpoint

This commit adds support for SSH endpoint monitoring. Users can now configure an endpoint to be monitored using an SSH command by prefixing the endpoint's URL with ssh:\\. The configuration options for an SSH endpoint include the username, password, and command to be executed on the remote server. In addition, two placeholders are supported for SSH endpoints: [CONNECTED] and [STATUS].

This commit also updates the README to include instructions on how to configure SSH endpoints and the placeholders that can be used in their conditions. The README has been updated to include the new SSH-related options in the endpoints[] configuration object.

Here's a summary of the changes made in this commit:

    Added support for SSH endpoint monitoring
    Updated the documentation to include instructions on how to configure SSH endpoints and the placeholders that can be used in their conditions
2023-09-23 13:37:24 -04:00
lolgopher
8fbfba2163 fix(alerting): Set correct email subject to resolved alert (#572)
fix(alerting): Set correct email subject to resolved alert (#566)
2023-09-21 21:36:42 -04:00
dependabot[bot]
0e9df7f00f chore(deps): bump github.com/miekg/dns from 1.1.54 to 1.1.55 (#545)
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.54 to 1.1.55.
- [Changelog](https://github.com/miekg/dns/blob/master/Makefile.release)
- [Commits](https://github.com/miekg/dns/compare/v1.1.54...v1.1.55)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-03 23:36:11 -04:00
Heitor
7d570ce148 websocket: handle error (#550)
Co-authored-by: TwiN <twin@linux.com>
2023-08-16 21:48:57 -04:00
plantatorbob
67941865db fix(alerting): correct success threshold for telegram (#551)
Co-authored-by: dave <dave@pootis.network>
2023-08-16 21:15:32 -04:00
I-HSIN Cheng
5c5a954b68 fix(tls): Honor client.insecure when doing TLS checks (#547)
* fix(watchdog): Add functions to avoid dangling file descriptors

* Change function name and add comment under core/endpoint.go
- change the function name of CloseHTTPConnection() to Close()
- add some comments above Close() function

* Update core/endpoint.go

* Update core/endpoint.go

* fix(client): Honor client.insecure when doing TLS checking
* add features in client/client.go to enable client.insecure when doing TLS checking

---------

Co-authored-by: Richard Cheng <richard_cheng@trendmicro.com>
Co-authored-by: TwiN <twin@linux.com>
2023-08-08 22:17:26 -04:00
Heitor
5f69351b6b feat: support monitoring WebSocket endpoints (#511)
* feat: support monitoring WebSocket endpoints

WebSocket endpoints are automatically identified by the URL protocol
specification: `wss://` or `ws://`. The request body is used as the
"message" written to the server, and the answer is stored in the
`[BODY]`.

Optionally, the user can set the `jsonrpc` flag to automatically wrap
the request body in a JSON RPC 2.0 method call.

* core:websocket: close connection after using it

* test: add tests related to WebSocket support

- test we can identify the endpoint type for WebSockets based on the URL
  supplied: `wss://` (with SSL/TLS) and `ws://` (plain text).
- test we can generate a JsonRPC 2.0 message via the new endpoint flag
  `JsonRPC`.

* core:endpoint: fix name of jsonrpc parameter

See https://en.wikipedia.org/wiki/JSON-RPC#Version_2.0

* core:websocket: fix dangling open connection on error

Move the `defer ws.Close()` to after opening the connection, so the
socket is closed also in case of errors.

* remove jsonrpc flag

* core:websocket: fix nil pointer dereference

The connection should only be closed if successfully opened.

* Move websocket function to client

* update go.mod

* Fix build errors

* Fix errors

* Update client/client.go

---------

Co-authored-by: TwiN <twin@linux.com>
2023-08-08 22:12:37 -04:00
I-HSIN Cheng
34313bec7e fix(watchdog): Close dangling file descriptors on shutdown and config reload (#544)
* fix(watchdog): Add functions to avoid dangling file descriptors

* Change function name and add comment under core/endpoint.go
- change the function name of CloseHTTPConnection() to Close()
- add some comments above Close() function

* Update core/endpoint.go

* Update core/endpoint.go

---------

Co-authored-by: Richard Cheng <richard_cheng@trendmicro.com>
Co-authored-by: TwiN <twin@linux.com>
2023-08-04 18:30:15 -04:00
dependabot[bot]
640e455d33 chore(deps): bump github.com/prometheus-community/pro-bing from 0.2.0 to 0.3.0 (#543)
chore(deps): bump github.com/prometheus-community/pro-bing

Bumps [github.com/prometheus-community/pro-bing](https://github.com/prometheus-community/pro-bing) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/prometheus-community/pro-bing/releases)
- [Changelog](https://github.com/prometheus-community/pro-bing/blob/main/.goreleaser.yaml)
- [Commits](https://github.com/prometheus-community/pro-bing/compare/v0.2.0...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus-community/pro-bing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-02 22:57:48 -04:00
dependabot[bot]
2be6a1e5f3 chore(deps): bump golang.org/x/crypto from 0.10.0 to 0.11.0 (#542)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/crypto/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-31 23:46:39 -04:00
dependabot[bot]
225e6ac7ae chore(deps): bump github.com/lib/pq from 1.10.7 to 1.10.9 (#537)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.7 to 1.10.9.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.7...v1.10.9)

---
updated-dependencies:
- dependency-name: github.com/lib/pq
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 22:03:36 -04:00
dependabot[bot]
87d9722621 chore(deps): bump modernc.org/sqlite from 1.23.1 to 1.24.0 (#528)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.23.1 to 1.24.0.
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.23.1...v1.24.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
2023-07-25 18:56:48 -04:00
44 changed files with 1440 additions and 259 deletions

View File

@@ -1,6 +1,6 @@
storage:
type: postgres
path: "postgres://username:password@postgres:5432/gatus?sslmode=disable"
path: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
endpoints:
- name: back-end

View File

@@ -18,6 +18,10 @@ services:
restart: always
ports:
- "8080:8080"
environment:
- POSTGRES_USER=username
- POSTGRES_PASSWORD=password
- POSTGRES_DB=gatus
volumes:
- ./config:/config
networks:

BIN
.github/assets/gotify-alerts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -25,6 +25,6 @@ jobs:
go-version: 1.19
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
ref: "${{ github.event.inputs.ref || 'master' }}"
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Benchmark
run: go test -bench=. ./storage/store

View File

@@ -5,20 +5,20 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Get image repository
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
platforms: linux/amd64
pull: true

View File

@@ -5,32 +5,32 @@ on:
branches: [master]
types: [completed]
concurrency:
group: ${{ github.workflow }}
group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }}::${{ github.workflow }}
cancel-in-progress: true
jobs:
publish-latest-to-ghcr:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
permissions:
contents: read
packages: write
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Get image repository
run: echo IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Login to Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true

View File

@@ -5,28 +5,28 @@ on:
branches: [master]
types: [completed]
concurrency:
group: ${{ github.workflow }}
group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }}::${{ github.workflow }}
cancel-in-progress: true
jobs:
publish-latest:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Get image repository
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true

View File

@@ -10,23 +10,23 @@ jobs:
packages: write
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Get image repository
run: echo IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Get the release
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
- name: Login to Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true

View File

@@ -8,22 +8,22 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Get image repository
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Get the release
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
- name: Login to Docker Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
pull: true

View File

@@ -14,12 +14,12 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 10
steps:
- uses: actions/setup-go@v4
with:
go-version: 1.19
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build binary to make sure it works
run: go build
- name: Test

267
README.md
View File

@@ -48,11 +48,13 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Storage](#storage)
- [Client configuration](#client-configuration)
- [Alerting](#alerting)
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
- [Configuring Discord alerts](#configuring-discord-alerts)
- [Configuring Email alerts](#configuring-email-alerts)
- [Configuring GitHub alerts](#configuring-github-alerts)
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
- [Configuring Gotify alerts](#configuring-gotify-alerts)
- [Configuring Matrix alerts](#configuring-matrix-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
@@ -87,8 +89,10 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Monitoring a TCP endpoint](#monitoring-a-tcp-endpoint)
- [Monitoring a UDP endpoint](#monitoring-a-udp-endpoint)
- [Monitoring a SCTP endpoint](#monitoring-a-sctp-endpoint)
- [Monitoring a WebSocket endpoint](#monitoring-a-websocket-endpoint)
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
- [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh)
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
- [Monitoring domain expiration](#monitoring-domain-expiration)
@@ -103,6 +107,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Health](#health)
- [Response time](#response-time)
- [API](#api)
- [Installing as binary](#installing-as-binary)
- [High level design overview](#high-level-design-overview)
- [Sponsors](#sponsors)
@@ -189,6 +194,8 @@ subdirectories are merged like so:
- To clarify, this also means that you could not define `alerting.slack.webhook-url` in two files with different values. All files are merged into one before they are processed. This is by design.
> 💡 You can also use environment variables in the configuration file (e.g. `$DOMAIN`, `${DOMAIN}`)
>
> See [examples/docker-compose-postgres-storage/config/config.yaml](.examples/docker-compose-postgres-storage/config/config.yaml) for an example.
If you want to test it locally, see [Docker](#docker).
@@ -196,54 +203,57 @@ If you want to test it locally, see [Docker](#docker).
## Configuration
| Parameter | Description | Default |
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
| `debug` | Whether to enable debug logs. | `false` |
| `metrics` | Whether to expose metrics at /metrics. | `false` |
| `storage` | [Storage configuration](#storage) | `{}` |
| `endpoints` | List of endpoints to monitor. | Required `[]` |
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
| `endpoints[].url` | URL to send the request to. | Required `""` |
| `endpoints[].method` | Request method. | `GET` |
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
| `endpoints[].body` | Request body. | `""` |
| `endpoints[].headers` | Request headers. | `{}` |
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
| `endpoints[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
| `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
| `alerting` | [Alerting configuration](#alerting). | `{}` |
| `security` | [Security configuration](#security). | `{}` |
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
| `web` | Web configuration. | `{}` |
| `web.address` | Address to listen on. | `0.0.0.0` |
| `web.port` | Port to listen on. | `8080` |
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
| `ui` | UI configuration. | `{}` |
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
| `ui.logo` | URL to the logo to display. | `""` |
| `ui.link` | Link to open when the logo is clicked. | `""` |
| `ui.buttons` | List of buttons to display below the header. | `[]` |
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
| `debug` | Whether to enable debug logs. | `false` |
| `metrics` | Whether to expose metrics at /metrics. | `false` |
| `storage` | [Storage configuration](#storage) | `{}` |
| `endpoints` | List of endpoints to monitor. | Required `[]` |
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
| `endpoints[].url` | URL to send the request to. | Required `""` |
| `endpoints[].method` | Request method. | `GET` |
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
| `endpoints[].body` | Request body. | `""` |
| `endpoints[].headers` | Request headers. | `{}` |
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
| `endpoints[].ssh` | Configuration for an endpoint of type SSH. <br />See [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh). | `""` |
| `endpoints[].ssh.username` | SSH username (e.g. example) | Required `""` |
| `endpoints[].ssh.password` | SSH password (e.g. password) | Required `""` |
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
| `endpoints[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
| `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
| `alerting` | [Alerting configuration](#alerting). | `{}` |
| `security` | [Security configuration](#security). | `{}` |
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
| `web` | Web configuration. | `{}` |
| `web.address` | Address to listen on. | `0.0.0.0` |
| `web.port` | Port to listen on. | `8080` |
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
| `ui` | UI configuration. | `{}` |
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
| `ui.logo` | URL to the logo to display. | `""` |
| `ui.link` | Link to open when the logo is clicked. | `""` |
| `ui.buttons` | List of buttons to display below the header. | `[]` |
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
### Conditions
@@ -415,6 +425,7 @@ ignored.
| `alerting.github` | Configuration for alerts of type `github`. <br />See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` |
| `alerting.gitlab` | Configuration for alerts of type `gitlab`. <br />See [Configuring GitLab alerts](#configuring-gitlab-alerts). | `{}` |
| `alerting.googlechat` | Configuration for alerts of type `googlechat`. <br />See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` |
| `alerting.gotify` | Configuration for alerts of type `gotify`. <br />See [Configuring Gotify alerts](#configuring-gotify-alerts). | `{}` |
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
@@ -433,6 +444,7 @@ ignored.
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
| `alerting.discord.title` | Title of the notification | `":helmet_with_white_cross: Gatus"` |
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.discord.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.discord.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
@@ -459,19 +471,20 @@ endpoints:
#### Configuring Email alerts
| Parameter | Description | Default |
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
| `alerting.email.from` | Email used to send the alert | Required `""` |
| `alerting.email.username` | Username of the SMTP server used to send the alert. If empty, uses `alerting.email.from`. | `""` |
| `alerting.email.password` | Password of the SMTP server used to send the alert | Required `""` |
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.email.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.email.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.email.overrides[].to` | Email(s) to send the alerts to | `""` |
| Parameter | Description | Default |
|:-----------------------------------|:----------------------------------------------------------------------------------------------|:--------------|
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
| `alerting.email.from` | Email used to send the alert | Required `""` |
| `alerting.email.username` | Username of the SMTP server used to send the alert. If empty, uses `alerting.email.from`. | `""` |
| `alerting.email.password` | Password of the SMTP server used to send the alert. If empty, no authentication is performed. | `""` |
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
| `alerting.email.client.insecure` | Whether to skip TLS verification | `false` |
| `alerting.email.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
| `alerting.email.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
| `alerting.email.overrides[].to` | Email(s) to send the alerts to | `""` |
```yaml
alerting:
@@ -482,6 +495,8 @@ alerting:
host: "mail.example.com"
port: 587
to: "recipient1@example.com,recipient2@example.com"
client:
insecure: false
# You can also add group-specific to keys, which will
# override the to key above for the specified groups
overrides:
@@ -626,6 +641,41 @@ endpoints:
```
#### Configuring Gotify alerts
| Parameter | Description | Default |
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:-----------------------|
| `alerting.gotify` | Configuration for alerts of type `gotify` | `{}` |
| `alerting.gotify.server-url` | Gotify server URL | Required `""` |
| `alerting.gotify.token` | Token that is used for authentication. | Required `""` |
| `alerting.gotify.priority` | Priority of the alert according to Gotify standarts. | `5` |
| `alerting.gotify.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
| `alerting.gotify.title` | Title of the notification | `"Gatus: <endpoint>"` |
```yaml
alerting:
gotify:
server-url: "https://gotify.example"
token: "**************"
endpoints:
- name: website
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 300"
alerts:
- type: gotify
description: "healthcheck failed"
send-on-resolved: true
```
Here's an example of what the notifications look like:
![Gotify notifications](.github/assets/gotify-alerts.png)
#### Configuring Matrix alerts
| Parameter | Description | Default |
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------|
@@ -1034,6 +1084,46 @@ endpoints:
```
#### Configuring AWS SES alerts
| Parameter | Description | Default |
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
| `alerting.aws-ses` | Settings for alerts of type `aws-ses` | `{}` |
| `alerting.aws-ses.access-key-id` | AWS Access Key ID | Optional `""` |
| `alerting.aws-ses.secret-access-key` | AWS Secret Access Key | Optional `""` |
| `alerting.aws-ses.region` | AWS Region | Required `""` |
| `alerting.aws-ses.from` | The Email address to send the emails from (should be registered in SES) | Required `""` |
| `alerting.aws-ses.to` | Comma separated list of email address to notify | Required `""` |
| `alerting.aws-ses.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
```yaml
alerting:
aws-ses:
access-key-id: "..."
secret-access-key: "..."
region: "us-east-1"
from: "status@example.com"
to: "user@example.com"
endpoints:
- name: website
interval: 30s
url: "https://twin.sh/health"
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 300"
alerts:
- type: aws-ses
failure-threshold: 5
send-on-resolved: true
description: "healthcheck failed"
```
If the `access-key-id` and `secret-access-key` are not defined Gatus will fall back to IAM authentication.
Make sure you have the ability to use `ses:SendEmail`.
#### Configuring custom alerts
| Parameter | Description | Default |
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
@@ -1531,6 +1621,22 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
This works for SCTP based application.
### Monitoring a WebSocket endpoint
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level:
```yaml
endpoints:
- name: example
url: "wss://example.com/"
body: "status"
conditions:
- "[CONNECTED] == true"
- "[BODY].result >= 0"
```
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
shows whether the connection was successfully established.
### Monitoring an endpoint using ICMP
By prefixing `endpoints[].url` with `icmp:\\`, you can monitor endpoints at a very basic level using ICMP, or more
commonly known as "ping" or "echo":
@@ -1568,6 +1674,28 @@ There are two placeholders that can be used in the conditions for endpoints of t
- The placeholder `[DNS_RCODE]` resolves to the name associated to the response code returned by the query, such as
`NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, etc.
### Monitoring an endpoint using SSH
You can monitor endpoints using SSH by prefixing `endpoints[].url` with `ssh:\\`:
```yaml
endpoints:
- name: ssh-example
url: "ssh://example.com:22" # port is optional. Default is 22.
ssh:
username: "username"
password: "password"
body: |
{
"command": "uptime"
}
interval: 1m
conditions:
- "[CONNECTED] == true"
- "[STATUS] == 0"
```
The following placeholders are supported for endpoints of type SSH:
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)
### Monitoring an endpoint using STARTTLS
If you have an email server that you want to ensure there are no problems with, monitoring it through STARTTLS
@@ -1584,7 +1712,6 @@ endpoints:
- "[CERTIFICATE_EXPIRATION] > 48h"
```
### Monitoring an endpoint using TLS
Monitoring endpoints using SSL/TLS encryption, such as LDAP over TLS, can help detect certificate expiration:
```yaml
@@ -1866,15 +1993,11 @@ The API will return a JSON payload with the `Content-Type` response header set t
No such header is required to query the API.
### Installing as binary
You can download Gatus as a binary using the following command:
```
go install github.com/TwiN/gatus/v5@latest
```
### High level design overview
![Gatus diagram](.github/assets/gatus-diagram.jpg)
## Sponsors
You can find the full list of sponsors [here](https://github.com/sponsors/TwiN).
<!-- _There is currently no sponsors_ -->
[<img src="https://github.com/8ball030.png" width="50" />](https://github.com/8ball030)
<!-- [<img src="https://github.com/$user.png" width="50" />](https://github.com/$user) -->

View File

@@ -5,6 +5,9 @@ package alert
type Type string
const (
// TypeAWSSES is the Type for the awsses alerting provider
TypeAWSSES Type = "aws-ses"
// TypeCustom is the Type for the custom alerting provider
TypeCustom Type = "custom"
@@ -23,6 +26,9 @@ const (
// TypeGoogleChat is the Type for the googlechat alerting provider
TypeGoogleChat Type = "googlechat"
// TypeGotify is the Type for the gotify alerting provider
TypeGotify Type = "gotify"
// TypeMatrix is the Type for the matrix alerting provider
TypeMatrix Type = "matrix"

View File

@@ -7,12 +7,14 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider"
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
"github.com/TwiN/gatus/v5/alerting/provider/custom"
"github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/alerting/provider/email"
"github.com/TwiN/gatus/v5/alerting/provider/github"
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
@@ -28,6 +30,9 @@ import (
// Config is the configuration for alerting providers
type Config struct {
// AWSSimpleEmailService is the configuration for the aws-ses alerting provider
AWSSimpleEmailService *awsses.AlertProvider `yaml:"aws-ses,omitempty"`
// Custom is the configuration for the custom alerting provider
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
@@ -46,6 +51,9 @@ type Config struct {
// GoogleChat is the configuration for the googlechat alerting provider
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
// Gotify is the configuration for the gotify alerting provider
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
// Matrix is the configuration for the matrix alerting provider
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
@@ -85,7 +93,8 @@ func (config *Config) GetAlertingProviderByAlertType(alertType alert.Type) provi
entityType := reflect.TypeOf(config).Elem()
for i := 0; i < entityType.NumField(); i++ {
field := entityType.Field(i)
if strings.ToLower(field.Name) == string(alertType) {
tag := strings.Split(field.Tag.Get("yaml"), ",")[0]
if tag == string(alertType) {
fieldValue := reflect.ValueOf(config).Elem().Field(i)
if fieldValue.IsNil() {
return nil

View File

@@ -0,0 +1,165 @@
package awsses
import (
"fmt"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
"strings"
)
const (
CharSet = "UTF-8"
)
// AlertProvider is the configuration necessary for sending an alert using AWS Simple Email Service
type AlertProvider struct {
AccessKeyID string `yaml:"access-key-id"`
SecretAccessKey string `yaml:"secret-access-key"`
Region string `yaml:"region"`
From string `yaml:"from"`
To string `yaml:"to"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
// Overrides is a list of Override that may be prioritized over the default configuration
Overrides []Override `yaml:"overrides,omitempty"`
}
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
To string `yaml:"to"`
}
// IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.To) == 0 {
return false
}
registeredGroups[override.Group] = true
}
}
// if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate,
// otherwise if neither are specified, then we'll fall back on IAM authentication.
return len(provider.From) > 0 && len(provider.To) > 0 &&
((len(provider.AccessKeyID) == 0 && len(provider.SecretAccessKey) == 0) || (len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0))
}
// Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
sess, err := provider.CreateSesSession()
if err != nil {
return err
}
svc := ses.New(sess)
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
input := &ses.SendEmailInput{
Destination: &ses.Destination{
ToAddresses: aws.StringSlice(emails),
},
Message: &ses.Message{
Body: &ses.Body{
Text: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(body),
},
},
Subject: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(subject),
},
},
Source: aws.String(provider.From),
}
_, err = svc.SendEmail(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ses.ErrCodeMessageRejected:
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
case ses.ErrCodeMailFromDomainNotVerifiedException:
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
case ses.ErrCodeConfigurationSetDoesNotExistException:
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return err
}
return nil
}
// buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
var subject, message, results string
if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else {
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
}
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = "✅"
} else {
prefix = "❌"
}
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
}
var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = "\n\nAlert description: " + alertDescription
}
return subject, message + description + "\n\nCondition results:\n" + results
}
// getToForGroup returns the appropriate email integration to for a given group
func (provider *AlertProvider) getToForGroup(group string) string {
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if group == override.Group {
return override.To
}
}
}
return provider.To
}
// GetDefaultAlert returns the provider's default alert configuration
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
func (provider AlertProvider) CreateSesSession() (*session.Session, error) {
config := &aws.Config{
Region: aws.String(provider.Region),
}
if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 {
config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "")
}
return session.NewSession(config)
}

View File

@@ -0,0 +1,188 @@
package awsses
import (
"testing"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core"
)
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
if invalidProvider.IsValid() {
t.Error("provider shouldn't have been valid")
}
invalidProviderWithOneKey := AlertProvider{From: "from@example.com", To: "to@example.com", AccessKeyID: "1"}
if invalidProviderWithOneKey.IsValid() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{From: "from@example.com", To: "to@example.com"}
if !validProvider.IsValid() {
t.Error("provider should've been valid")
}
validProviderWithKeys := AlertProvider{From: "from@example.com", To: "to@example.com", AccessKeyID: "1", SecretAccessKey: "1"}
if !validProviderWithKeys.IsValid() {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
providerWithInvalidOverrideGroup := AlertProvider{
Overrides: []Override{
{
To: "to@example.com",
Group: "",
},
},
}
if providerWithInvalidOverrideGroup.IsValid() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
Overrides: []Override{
{
To: "",
Group: "group",
},
},
}
if providerWithInvalidOverrideTo.IsValid() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
From: "from@example.com",
To: "to@example.com",
Overrides: []Override{
{
To: "to@example.com",
Group: "group",
},
},
}
if !providerWithValidOverride.IsValid() {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_buildRequestBody(t *testing.T) {
firstDescription := "description-1"
secondDescription := "description-2"
scenarios := []struct {
Name string
Provider AlertProvider
Alert alert.Alert
Resolved bool
ExpectedSubject string
ExpectedBody string
}{
{
Name: "triggered",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedSubject: "[endpoint-name] Alert triggered",
ExpectedBody: "An alert for endpoint-name has been triggered due to having failed 3 time(s) in a row\n\nAlert description: description-1\n\nCondition results:\n❌ [CONNECTED] == true\n❌ [STATUS] == 200\n",
},
{
Name: "resolved",
Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedSubject: "[endpoint-name] Alert resolved",
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
subject, body := scenario.Provider.buildMessageSubjectAndBody(
&core.Endpoint{Name: "endpoint-name"},
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if subject != scenario.ExpectedSubject {
t.Errorf("expected subject to be %s, got %s", scenario.ExpectedSubject, subject)
}
if body != scenario.ExpectedBody {
t.Errorf("expected body to be %s, got %s", scenario.ExpectedBody, body)
}
})
}
}
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
t.Error("expected default alert to be not nil")
}
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
t.Error("expected default alert to be nil")
}
}
func TestAlertProvider_getToForGroup(t *testing.T) {
tests := []struct {
Name string
Provider AlertProvider
InputGroup string
ExpectedOutput string
}{
{
Name: "provider-no-override-specify-no-group-should-default",
Provider: AlertProvider{
To: "to@example.com",
Overrides: nil,
},
InputGroup: "",
ExpectedOutput: "to@example.com",
},
{
Name: "provider-no-override-specify-group-should-default",
Provider: AlertProvider{
To: "to@example.com",
Overrides: nil,
},
InputGroup: "group",
ExpectedOutput: "to@example.com",
},
{
Name: "provider-with-override-specify-no-group-should-default",
Provider: AlertProvider{
To: "to@example.com",
Overrides: []Override{
{
Group: "group",
To: "to01@example.com",
},
},
},
InputGroup: "",
ExpectedOutput: "to@example.com",
},
{
Name: "provider-with-override-specify-group-should-override",
Provider: AlertProvider{
To: "to@example.com",
Overrides: []Override{
{
Group: "group",
To: "to01@example.com",
},
},
},
InputGroup: "group",
ExpectedOutput: "to01@example.com",
},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
if got := tt.Provider.getToForGroup(tt.InputGroup); got != tt.ExpectedOutput {
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
}
})
}
}

View File

@@ -21,6 +21,9 @@ type AlertProvider struct {
// Overrides is a list of Override that may be prioritized over the default configuration
Overrides []Override `yaml:"overrides,omitempty"`
// Title is the title of the message that will be sent
Title string `yaml:"title,omitempty"`
}
// Override is a case under which the default integration is overridden
@@ -105,11 +108,15 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":\n> " + alertDescription
}
title := ":helmet_with_white_cross: Gatus"
if provider.Title != "" {
title = provider.Title
}
body, _ := json.Marshal(Body{
Content: "",
Embeds: []Embed{
{
Title: ":helmet_with_white_cross: Gatus",
Title: title,
Description: message + description,
Color: colorCode,
Fields: []Field{

View File

@@ -63,6 +63,7 @@ func TestAlertProvider_Send(t *testing.T) {
defer client.InjectHTTPClient(nil)
firstDescription := "description-1"
secondDescription := "description-2"
title := "provider-title"
scenarios := []struct {
Name string
Provider AlertProvider
@@ -111,6 +112,16 @@ func TestAlertProvider_Send(t *testing.T) {
}),
ExpectedError: true,
},
{
Name: "triggered-with-modified-title",
Provider: AlertProvider{Title: title},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
}),
ExpectedError: false,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
@@ -139,6 +150,7 @@ func TestAlertProvider_Send(t *testing.T) {
func TestAlertProvider_buildRequestBody(t *testing.T) {
firstDescription := "description-1"
secondDescription := "description-2"
title := "provider-title"
scenarios := []struct {
Name string
Provider AlertProvider
@@ -160,6 +172,13 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: true,
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"color\":3066993,\"fields\":[{\"name\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n:white_check_mark: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
},
{
Name: "triggered-with-modified-title",
Provider: AlertProvider{Title: title},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332,\"fields\":[{\"name\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n:x: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {

View File

@@ -1,11 +1,13 @@
package email
import (
"crypto/tls"
"fmt"
"math"
"strings"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core"
gomail "gopkg.in/mail.v2"
)
@@ -19,6 +21,9 @@ type AlertProvider struct {
Port int `yaml:"port"`
To string `yaml:"to"`
// ClientConfig is the configuration of the client used to communicate with the provider's target
ClientConfig *client.Config `yaml:"client,omitempty"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -44,7 +49,7 @@ func (provider *AlertProvider) IsValid() bool {
}
}
return len(provider.From) > 0 && len(provider.Password) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
return len(provider.From) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
}
// Send an alert using the provider
@@ -61,17 +66,34 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
m.SetHeader("Subject", subject)
m.SetBody("text/plain", body)
d := gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
var d *gomail.Dialer
if len(provider.Password) == 0 {
// Get the domain in the From address
localName := "localhost"
fromParts := strings.Split(provider.From, `@`)
if len(fromParts) == 2 {
localName = fromParts[1]
}
// Create a dialer with no authentication
d = &gomail.Dialer{Host: provider.Host, Port: provider.Port, LocalName: localName}
} else {
// Create an authenticated dialer
d = gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
}
if provider.ClientConfig != nil && provider.ClientConfig.Insecure {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
return d.DialAndSend(m)
}
// buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
var message, results string
subject := fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
var subject, message, results string
if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else {
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
}
for _, conditionResult := range result.ConditionResults {

View File

@@ -18,6 +18,13 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
}
}
func TestAlertProvider_IsValidWithNoCredentials(t *testing.T) {
validProvider := AlertProvider{From: "from@example.com", Host: "smtp-relay.gmail.com", Port: 587, To: "to@example.com"}
if !validProvider.IsValid() {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
providerWithInvalidOverrideGroup := AlertProvider{
Overrides: []Override{
@@ -83,7 +90,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedSubject: "[endpoint-name] Alert triggered",
ExpectedSubject: "[endpoint-name] Alert resolved",
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
},
}

View File

@@ -0,0 +1,105 @@
package gotify
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core"
)
const DefaultPriority = 5
// AlertProvider is the configuration necessary for sending an alert using Gotify
type AlertProvider struct {
// ServerURL is the URL of the Gotify server
ServerURL string `yaml:"server-url"`
// Token is the token to use when sending a message to the Gotify server
Token string `yaml:"token"`
// Priority is the priority of the message
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
// Title is the title of the message that will be sent
Title string `yaml:"title,omitempty"`
}
// IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool {
if provider.Priority == 0 {
provider.Priority = DefaultPriority
}
return len(provider.ServerURL) > 0 && len(provider.Token) > 0
}
// Send an alert using the provider
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode > 399 {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("failed to send alert to Gotify: %s", string(body))
}
return nil
}
type Body struct {
Message string `json:"message"`
Title string `json:"title"`
Priority int `json:"priority"`
}
// buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
var message, results string
if resolved {
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else {
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
}
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = "✓"
} else {
prefix = "✕"
}
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
}
if len(alert.GetDescription()) > 0 {
message += " with the following description: " + alert.GetDescription()
}
message += results
title := "Gatus: " + endpoint.DisplayName()
if provider.Title != "" {
title = provider.Title
}
body, _ := json.Marshal(Body{
Message: message,
Title: title,
Priority: provider.Priority,
})
return body
}
// GetDefaultAlert returns the provider's default alert configuration
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}

View File

@@ -0,0 +1,105 @@
package gotify
import (
"encoding/json"
"fmt"
"testing"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/core"
)
func TestAlertProvider_IsValid(t *testing.T) {
scenarios := []struct {
name string
provider AlertProvider
expected bool
}{
{
name: "valid",
provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
expected: true,
},
{
name: "invalid-server-url",
provider: AlertProvider{ServerURL: "", Token: "faketoken"},
expected: false,
},
{
name: "invalid-app-token",
provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: ""},
expected: false,
},
{
name: "no-priority-should-use-default-value",
provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
expected: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
if scenario.provider.IsValid() != scenario.expected {
t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.IsValid())
}
})
}
}
func TestAlertProvider_buildRequestBody(t *testing.T) {
var (
description = "custom-description"
//title = "custom-title"
endpoint = "custom-endpoint"
)
scenarios := []struct {
Name string
Provider AlertProvider
Alert alert.Alert
Resolved bool
ExpectedBody string
}{
{
Name: "triggered",
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
},
{
Name: "resolved",
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
},
{
Name: "custom-title",
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpoint, description),
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: endpoint},
&scenario.Alert,
&core.Result{
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved,
)
if string(body) != scenario.ExpectedBody {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
}
out := make(map[string]interface{})
if err := json.Unmarshal(body, &out); err != nil {
t.Error("expected body to be valid JSON, got error:", err.Error())
}
})
}
}

View File

@@ -78,17 +78,27 @@ type Body struct {
// buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
var message, tag string
var message, results, tag string
if resolved {
tag = "white_check_mark"
message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row"
} else {
tag = "x"
tag = "rotating_light"
message = "An alert has been triggered due to having failed " + strconv.Itoa(alert.FailureThreshold) + " time(s) in a row"
}
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = "🟢"
} else {
prefix = "🔴"
}
results += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition)
}
if len(alert.GetDescription()) > 0 {
message += " with the following description: " + alert.GetDescription()
}
message += results
body, _ := json.Marshal(Body{
Topic: provider.Topic,
Title: "Gatus: " + endpoint.DisplayName(),

View File

@@ -79,14 +79,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1","tags":["x"],"priority":1}`,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1}`,
},
{
Name: "resolved",
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2","tags":["white_check_mark"],"priority":2}`,
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`,
},
}
for _, scenario := range scenarios {

View File

@@ -2,6 +2,7 @@ package provider
import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
"github.com/TwiN/gatus/v5/alerting/provider/custom"
"github.com/TwiN/gatus/v5/alerting/provider/discord"
"github.com/TwiN/gatus/v5/alerting/provider/email"
@@ -58,6 +59,7 @@ func ParseWithDefaultAlert(providerDefaultAlert, endpointAlert *alert.Alert) {
var (
// Validate interface implementation on compile
_ AlertProvider = (*awsses.AlertProvider)(nil)
_ AlertProvider = (*custom.AlertProvider)(nil)
_ AlertProvider = (*discord.AlertProvider)(nil)
_ AlertProvider = (*email.AlertProvider)(nil)

View File

@@ -69,7 +69,7 @@ type Body struct {
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
var message, results string
if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.SuccessThreshold)
} else {
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
}

View File

@@ -131,7 +131,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider: AlertProvider{ID: "123"},
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: true,
ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 3 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}",
ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 5 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}",
},
}
for _, scenario := range scenarios {

View File

@@ -3,6 +3,7 @@ package client
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net"
@@ -16,6 +17,8 @@ import (
"github.com/TwiN/whois"
"github.com/ishidawataru/sctp"
ping "github.com/prometheus-community/pro-bing"
"golang.org/x/crypto/ssh"
"golang.org/x/net/websocket"
)
var (
@@ -142,18 +145,92 @@ func CanPerformStartTLS(address string, config *Config) (connected bool, certifi
// CanPerformTLS checks whether a connection can be established to an address using the TLS protocol
func CanPerformTLS(address string, config *Config) (connected bool, certificate *x509.Certificate, err error) {
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, nil)
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, &tls.Config{
InsecureSkipVerify: config.Insecure,
})
if err != nil {
return
}
defer connection.Close()
verifiedChains := connection.ConnectionState().VerifiedChains
// If config.Insecure is set to true, verifiedChains will be an empty list []
// We should get the parsed certificates from PeerCertificates, it can't be empty on the client side
// Reference: https://pkg.go.dev/crypto/tls#PeerCertificates
if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
return
peerCertificates := connection.ConnectionState().PeerCertificates
return true, peerCertificates[0], nil
}
return true, verifiedChains[0][0], nil
}
// CanCreateSSHConnection checks whether a connection can be established and a command can be executed to an address
// using the SSH protocol.
func CanCreateSSHConnection(address, username, password string, config *Config) (bool, *ssh.Client, error) {
var port string
if strings.Contains(address, ":") {
addressAndPort := strings.Split(address, ":")
if len(addressAndPort) != 2 {
return false, nil, errors.New("invalid address for ssh, format must be host:port")
}
address = addressAndPort[0]
port = addressAndPort[1]
} else {
port = "22"
}
cli, err := ssh.Dial("tcp", strings.Join([]string{address, port}, ":"), &ssh.ClientConfig{
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
Timeout: config.Timeout,
})
if err != nil {
return false, nil, err
}
return true, cli, nil
}
// ExecuteSSHCommand executes a command to an address using the SSH protocol.
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) {
type Body struct {
Command string `json:"command"`
}
defer sshClient.Close()
var b Body
if err := json.Unmarshal([]byte(body), &b); err != nil {
return false, 0, err
}
sess, err := sshClient.NewSession()
if err != nil {
return false, 0, err
}
err = sess.Start(b.Command)
if err != nil {
return false, 0, err
}
defer sess.Close()
err = sess.Wait()
if err == nil {
return true, 0, nil
}
e, ok := err.(*ssh.ExitError)
if !ok {
return false, 0, err
}
return true, e.ExitStatus(), nil
}
// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged
//
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
@@ -184,6 +261,38 @@ func Ping(address string, config *Config) (bool, time.Duration) {
return true, 0
}
// QueryWebSocket opens a websocket connection, write `body` and return a message from the server
func QueryWebSocket(address, body string, config *Config) (bool, []byte, error) {
const (
Origin = "http://localhost/"
MaximumMessageSize = 1024 // in bytes
)
wsConfig, err := websocket.NewConfig(address, Origin)
if err != nil {
return false, nil, fmt.Errorf("error configuring websocket connection: %w", err)
}
if config != nil {
wsConfig.Dialer = &net.Dialer{Timeout: config.Timeout}
}
// Dial URL
ws, err := websocket.DialConfig(wsConfig)
if err != nil {
return false, nil, fmt.Errorf("error dialing websocket: %w", err)
}
defer ws.Close()
// Write message
if _, err := ws.Write([]byte(body)); err != nil {
return false, nil, fmt.Errorf("error writing websocket body: %w", err)
}
// Read message
var n int
msg := make([]byte, MaximumMessageSize)
if n, err = ws.Read(msg); err != nil {
return false, nil, fmt.Errorf("error reading websocket message: %w", err)
}
return true, msg[:n], nil
}
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
func InjectHTTPClient(httpClient *http.Client) {
injectedHTTPClient = httpClient

View File

@@ -253,3 +253,14 @@ func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
t.Error("exptected `secret-token` as Bearer token in the mocked response header `X-Org-Authorization`, but got", response.Header.Get("X-Org-Authorization"))
}
}
func TestQueryWebSocket(t *testing.T) {
_, _, err := QueryWebSocket("", "body", &Config{Timeout: 2 * time.Second})
if err == nil {
t.Error("expected an error due to the address being invalid")
}
_, _, err = QueryWebSocket("ws://example.org", "body", &Config{Timeout: 2 * time.Second})
if err == nil {
t.Error("expected an error due to the target not being websocket-friendly")
}
}

View File

@@ -16,7 +16,7 @@ import (
)
const (
defaultHTTPTimeout = 10 * time.Second
defaultTimeout = 10 * time.Second
)
var (
@@ -27,7 +27,7 @@ var (
defaultConfig = Config{
Insecure: false,
IgnoreRedirect: false,
Timeout: defaultHTTPTimeout,
Timeout: defaultTimeout,
}
)

View File

@@ -13,7 +13,7 @@ func TestConfig_getHTTPClient(t *testing.T) {
if !(insecureClient.Transport).(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Error("expected Config.Insecure set to true to cause the HTTP client to skip certificate verification")
}
if insecureClient.Timeout != defaultHTTPTimeout {
if insecureClient.Timeout != defaultTimeout {
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
}
request, _ := http.NewRequest("GET", "", nil)

View File

@@ -361,11 +361,13 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
return
}
alertTypes := []alert.Type{
alert.TypeAWSSES,
alert.TypeCustom,
alert.TypeDiscord,
alert.TypeGitHub,
alert.TypeGitLab,
alert.TypeGoogleChat,
alert.TypeGotify,
alert.TypeEmail,
alert.TypeMatrix,
alert.TypeMattermost,

View File

@@ -224,6 +224,14 @@ func isEqual(first, second string) bool {
return false
}
}
// test if inputs are integers
firstInt, err1 := strconv.ParseInt(first, 0, 64)
secondInt, err2 := strconv.ParseInt(second, 0, 64)
if err1 == nil && err2 == nil {
return firstInt == secondInt
}
return first == second
}
@@ -304,7 +312,7 @@ func sanitizeAndResolveNumerical(list []string, result *Result) (parameters []st
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
// If the string is a duration, convert it to milliseconds
resolvedNumericalParameters = append(resolvedNumericalParameters, duration.Milliseconds())
} else if number, err := strconv.ParseInt(element, 10, 64); err != nil {
} else if number, err := strconv.ParseInt(element, 0, 64); err != nil {
// It's not an int, so we'll check if it's a float
if f, err := strconv.ParseFloat(element, 64); err == nil {
// It's a float, but we'll convert it to an int. We're losing precision here, but it's better than

View File

@@ -287,6 +287,111 @@ func TestCondition_evaluate(t *testing.T) {
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data.id > 0",
},
{
Name: "body-jsonpath-hexadecimal-int-using-greater-than",
Condition: Condition("[BODY].data > 0"),
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data > 0",
},
{
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0x1",
Condition: Condition("[BODY].data == 1"),
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 1",
},
{
Name: "body-jsonpath-hexadecimal-int-using-equals",
Condition: Condition("[BODY].data == 0x1"),
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 0x1",
},
{
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0x2",
Condition: Condition("[BODY].data == 2"),
Result: &Result{Body: []byte("{\"data\": \"0x2\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 2",
},
{
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0xF",
Condition: Condition("[BODY].data == 15"),
Result: &Result{Body: []byte("{\"data\": \"0xF\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 15",
},
{
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0xC0ff33",
Condition: Condition("[BODY].data == 12648243"),
Result: &Result{Body: []byte("{\"data\": \"0xC0ff33\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 12648243",
},
{
Name: "body-jsonpath-hexadecimal-int-len",
Condition: Condition("len([BODY].data) == 3"),
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
ExpectedSuccess: true,
ExpectedOutput: "len([BODY].data) == 3",
},
{
Name: "body-jsonpath-hexadecimal-int-greater",
Condition: Condition("[BODY].data >= 1"),
Result: &Result{Body: []byte("{\"data\": \"0x01\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data >= 1",
},
{
Name: "body-jsonpath-hexadecimal-int-0x01-len",
Condition: Condition("len([BODY].data) == 4"),
Result: &Result{Body: []byte("{\"data\": \"0x01\"}")},
ExpectedSuccess: true,
ExpectedOutput: "len([BODY].data) == 4",
},
{
Name: "body-jsonpath-octal-int-using-greater-than",
Condition: Condition("[BODY].data > 0"),
Result: &Result{Body: []byte("{\"data\": \"0o1\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data > 0",
},
{
Name: "body-jsonpath-octal-int-using-equal",
Condition: Condition("[BODY].data == 2"),
Result: &Result{Body: []byte("{\"data\": \"0o2\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 2",
},
{
Name: "body-jsonpath-octal-int-using-equals",
Condition: Condition("[BODY].data == 0o2"),
Result: &Result{Body: []byte("{\"data\": \"0o2\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 0o2",
},
{
Name: "body-jsonpath-binary-int-using-greater-than",
Condition: Condition("[BODY].data > 0"),
Result: &Result{Body: []byte("{\"data\": \"0b1\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data > 0",
},
{
Name: "body-jsonpath-binary-int-using-equal",
Condition: Condition("[BODY].data == 2"),
Result: &Result{Body: []byte("{\"data\": \"0b0010\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 2",
},
{
Name: "body-jsonpath-binary-int-using-equals",
Condition: Condition("[BODY].data == 0b10"),
Result: &Result{Body: []byte("{\"data\": \"0b0010\"}")},
ExpectedSuccess: true,
ExpectedOutput: "[BODY].data == 0b10",
},
{
Name: "body-jsonpath-complex-int-using-greater-than-failure",
Condition: Condition("[BODY].data.id > 5"),

View File

@@ -17,6 +17,7 @@ import (
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/core/ui"
"github.com/TwiN/gatus/v5/util"
"golang.org/x/crypto/ssh"
)
type EndpointType string
@@ -42,6 +43,8 @@ const (
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
EndpointTypeTLS EndpointType = "TLS"
EndpointTypeHTTP EndpointType = "HTTP"
EndpointTypeWS EndpointType = "WEBSOCKET"
EndpointTypeSSH EndpointType = "SSH"
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
)
@@ -69,6 +72,10 @@ var (
// This is because the free whois service we are using should not be abused, especially considering the fact that
// the data takes a while to be updated.
ErrInvalidEndpointIntervalForDomainExpirationPlaceholder = errors.New("the minimum interval for an endpoint with a condition using the " + DomainExpirationPlaceholder + " placeholder is 300s (5m)")
// ErrEndpointWithoutSSHUsername is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a user.
ErrEndpointWithoutSSHUsername = errors.New("you must specify a username for each endpoint with SSH")
// ErrEndpointWithoutSSHPassword is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a password.
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each endpoint with SSH")
)
// Endpoint is the configuration of a monitored
@@ -120,6 +127,27 @@ type Endpoint struct {
// NumberOfSuccessesInARow is the number of successful evaluations in a row
NumberOfSuccessesInARow int `yaml:"-"`
// SSH is the configuration of SSH monitoring.
SSH *SSH `yaml:"ssh,omitempty"`
}
type SSH struct {
// Username is the username to use when connecting to the SSH server.
Username string `yaml:"username,omitempty"`
// Password is the password to use when connecting to the SSH server.
Password string `yaml:"password,omitempty"`
}
// ValidateAndSetDefaults validates the endpoint
func (s *SSH) ValidateAndSetDefaults() error {
if s.Username == "" {
return ErrEndpointWithoutSSHUsername
}
if s.Password == "" {
return ErrEndpointWithoutSSHPassword
}
return nil
}
// IsEnabled returns whether the endpoint is enabled or not
@@ -149,6 +177,10 @@ func (endpoint Endpoint) Type() EndpointType {
return EndpointTypeTLS
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
return EndpointTypeHTTP
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
return EndpointTypeWS
case strings.HasPrefix(endpoint.URL, "ssh://"):
return EndpointTypeSSH
default:
return EndpointTypeUNKNOWN
}
@@ -225,6 +257,9 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
if err != nil {
return err
}
if endpoint.SSH != nil {
return endpoint.SSH.ValidateAndSetDefaults()
}
return nil
}
@@ -340,6 +375,26 @@ func (endpoint *Endpoint) call(result *Result) {
result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeICMP {
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
} else if endpointType == EndpointTypeWS {
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig)
if err != nil {
result.AddError(err.Error())
return
}
result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeSSH {
var cli *ssh.Client
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
if err != nil {
result.AddError(err.Error())
return
}
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
if err != nil {
result.AddError(err.Error())
return
}
result.Duration = time.Since(startTime)
} else {
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
result.Duration = time.Since(startTime)
@@ -364,6 +419,15 @@ func (endpoint *Endpoint) call(result *Result) {
}
}
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
// on configuration reload.
// More context on https://github.com/TwiN/gatus/issues/536
func (endpoint *Endpoint) Close() {
if endpoint.Type() == EndpointTypeHTTP {
client.GetHTTPClient(endpoint.ClientConfig).CloseIdleConnections()
}
}
func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
var bodyBuffer *bytes.Buffer
if endpoint.GraphQL {

View File

@@ -256,6 +256,7 @@ func TestEndpoint_Type(t *testing.T) {
type args struct {
URL string
DNS *DNS
SSH *SSH
}
tests := []struct {
args args
@@ -313,6 +314,28 @@ func TestEndpoint_Type(t *testing.T) {
},
want: EndpointTypeHTTP,
},
{
args: args{
URL: "wss://example.com/",
},
want: EndpointTypeWS,
},
{
args: args{
URL: "ws://example.com/",
},
want: EndpointTypeWS,
},
{
args: args{
URL: "ssh://example.com:22",
SSH: &SSH{
Username: "root",
Password: "password",
},
},
want: EndpointTypeSSH,
},
{
args: args{
URL: "invalid://example.org",
@@ -442,6 +465,52 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
}
}
func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
tests := []struct {
name string
username string
password string
expectedErr error
}{
{
name: "fail when has no user",
username: "",
password: "password",
expectedErr: ErrEndpointWithoutSSHUsername,
},
{
name: "fail when has no password",
username: "username",
password: "",
expectedErr: ErrEndpointWithoutSSHPassword,
},
{
name: "success when all fields are set",
username: "username",
password: "password",
expectedErr: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
endpoint := &Endpoint{
Name: "ssh-test",
URL: "https://example.com",
SSH: &SSH{
Username: test.username,
Password: test.password,
},
Conditions: []Condition{Condition("[STATUS] == 0")},
}
err := endpoint.ValidateAndSetDefaults()
if err != test.expectedErr {
t.Errorf("expected error %v, got %v", test.expectedErr, err)
}
})
}
}
func TestEndpoint_ValidateAndSetDefaultsWithSimpleErrors(t *testing.T) {
scenarios := []struct {
endpoint *Endpoint
@@ -668,6 +737,55 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
}
}
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
tests := []struct {
name string
endpoint Endpoint
conditions []Condition
success bool
}{
{
name: "ssh-success",
endpoint: Endpoint{
Name: "ssh-success",
URL: "ssh://localhost",
SSH: &SSH{
Username: "test",
Password: "test",
},
Body: "{ \"command\": \"uptime\" }",
},
conditions: []Condition{Condition("[STATUS] == 0")},
success: true,
},
{
name: "ssh-failure",
endpoint: Endpoint{
Name: "ssh-failure",
URL: "ssh://localhost",
SSH: &SSH{
Username: "test",
Password: "test",
},
Body: "{ \"command\": \"uptime\" }",
},
conditions: []Condition{Condition("[STATUS] == 1")},
success: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.endpoint.ValidateAndSetDefaults()
test.endpoint.Conditions = test.conditions
result := test.endpoint.EvaluateHealth()
if result.Success != test.success {
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
}
})
}
}
func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
endpoint := Endpoint{
Name: "icmp-test",

63
go.mod
View File

@@ -7,28 +7,31 @@ require (
github.com/TwiN/g8/v2 v2.0.0
github.com/TwiN/gocache/v2 v2.2.0
github.com/TwiN/health v1.6.0
github.com/TwiN/whois v1.1.3
github.com/coreos/go-oidc/v3 v3.6.0
github.com/gofiber/fiber/v2 v2.46.0
github.com/TwiN/whois v1.1.7
github.com/aws/aws-sdk-go v1.47.9
github.com/coreos/go-oidc/v3 v3.7.0
github.com/gofiber/fiber/v2 v2.49.2
github.com/google/go-github/v48 v48.2.0
github.com/google/uuid v1.3.0
github.com/google/uuid v1.3.1
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062
github.com/lib/pq v1.10.7
github.com/miekg/dns v1.1.54
github.com/prometheus-community/pro-bing v0.2.0
github.com/prometheus/client_golang v1.16.0
github.com/valyala/fasthttp v1.48.0
github.com/wcharczuk/go-chart/v2 v2.1.0
golang.org/x/crypto v0.10.0
golang.org/x/oauth2 v0.8.0
github.com/lib/pq v1.10.9
github.com/miekg/dns v1.1.56
github.com/prometheus-community/pro-bing v0.3.0
github.com/prometheus/client_golang v1.17.0
github.com/valyala/fasthttp v1.50.0
github.com/wcharczuk/go-chart/v2 v2.1.1
golang.org/x/crypto v0.14.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.13.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.23.1
modernc.org/sqlite v1.26.0
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blend/go-sdk v1.20220411.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -36,40 +39,36 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/image v0.5.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/tools v0.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/image v0.11.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect

164
go.sum
View File

@@ -6,16 +6,20 @@ github.com/TwiN/gocache/v2 v2.2.0 h1:M3B36KyH24BntxLrLaUb2kgTdq8DzCnfod0IekLG57w
github.com/TwiN/gocache/v2 v2.2.0/go.mod h1:SnUuBsrwGQeNcDG6vhkOMJnqErZM0JGjgIkuKryokYA=
github.com/TwiN/health v1.6.0 h1:L2ks575JhRgQqWWOfKjw9B0ec172hx7GdToqkYUycQM=
github.com/TwiN/health v1.6.0/go.mod h1:Z6TszwQPMvtSiVx1QMidVRgvVr4KZGfiwqcD7/Z+3iw=
github.com/TwiN/whois v1.1.3 h1:DOexOmaCGuPCzzjcykE5MQQEiVawHHIIhVRyPUxbvWg=
github.com/TwiN/whois v1.1.3/go.mod h1:VOJAH4+3chAik5gva5zxJNXv2voEHjMNCf1y07sqj9w=
github.com/TwiN/whois v1.1.7 h1:eGzLOrWhpYLAGXD8boXh0bBKllN/EmuBsLqTJT4tC/U=
github.com/TwiN/whois v1.1.7/go.mod h1:VOJAH4+3chAik5gva5zxJNXv2voEHjMNCf1y07sqj9w=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aws/aws-sdk-go v1.47.9 h1:rarTsos0mA16q+huicGx0e560aYRtOucV5z2Mw23JRY=
github.com/aws/aws-sdk-go v1.47.9/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/coreos/go-oidc/v3 v3.7.0 h1:FTdj0uexT4diYIPlF4yoFVI5MRO1r5+SEcIpEw9vC0o=
github.com/coreos/go-oidc/v3 v3.7.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -24,14 +28,13 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns=
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -43,46 +46,47 @@ github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 h1:G1+wBT0dwjIrBdLy0MIG0i+E4CQxEnedHXdauJEIH6g=
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.2.0 h1:hyK7yPFndU3LCDwEQJwPQUCjNkp1DGP/VxyzrWfXZUU=
github.com/prometheus-community/pro-bing v0.2.0/go.mod h1:20arNb2S8rNG3EtmjHyZZU92cfbhQx7oCHZ9sulAV+I=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus-community/pro-bing v0.3.0 h1:SFT6gHqXwbItEDJhTkzPWVqU6CLEtqEfNAPp47RUON4=
github.com/prometheus-community/pro-bing v0.3.0/go.mod h1:p9dLb9zdmv+eLxWfCT6jESWuDrS+YzpPkQBgysQF8a0=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -91,103 +95,87 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -199,16 +187,16 @@ modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw=
modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=

View File

@@ -27,7 +27,7 @@ func main() {
go func() {
<-signalChannel
log.Println("Received termination signal, attempting to gracefully shut down")
stop()
stop(cfg)
save()
done <- true
}()
@@ -41,8 +41,8 @@ func start(cfg *config.Config) {
go listenToConfigurationFileChanges(cfg)
}
func stop() {
watchdog.Shutdown()
func stop(cfg *config.Config) {
watchdog.Shutdown(cfg)
controller.Shutdown()
}
@@ -89,7 +89,7 @@ func listenToConfigurationFileChanges(cfg *config.Config) {
time.Sleep(30 * time.Second)
if cfg.HasLoadedConfigurationBeenModified() {
log.Println("[main][listenToConfigurationFileChanges] Configuration file has been modified")
stop()
stop(cfg)
time.Sleep(time.Second) // Wait a bit to make sure everything is done.
save()
updatedConfig, err := loadConfiguration()
@@ -104,6 +104,7 @@ func listenToConfigurationFileChanges(cfg *config.Config) {
panic(err)
}
}
store.Get().Close()
initializeStorage(updatedConfig)
start(updatedConfig)
return

View File

@@ -96,6 +96,10 @@ func UpdateEndpointStatuses(endpoint *core.Endpoint, result *core.Result) {
}
// Shutdown stops monitoring all endpoints
func Shutdown() {
func Shutdown(cfg *config.Config) {
// Disable all the old HTTP connections
for _, endpoint := range cfg.Endpoints {
endpoint.Close()
}
cancelFunc()
}

View File

@@ -7,9 +7,9 @@
<div class="text-3xl xl:text-5xl lg:text-4xl font-light">{{ header }}</div>
</div>
<div class="w-1/4 flex justify-end">
<component :is="link ? 'a' : 'div'" :href="link" target="_blank" style="width:100px">
<img v-if="logo" :src="logo" alt="Gatus" class="object-scale-down" style="max-width: 100px; min-width: 50px; min-height:50px;" />
<img v-else src="./assets/logo.svg" alt="Gatus" class="object-scale-down" style="max-width: 100px; min-width: 50px; min-height:50px;" />
<component :is="link ? 'a' : 'div'" :href="link" target="_blank" class="flex items-center justify-center" style="width:100px;min-height:100px;">
<img v-if="logo" :src="logo" alt="Gatus" class="object-scale-down" style="max-width:100px;min-width:50px;min-height:50px;" />
<img v-else src="./assets/logo.svg" alt="Gatus" class="object-scale-down" style="max-width:100px;min-width:50px;min-height:50px;" />
</component>
</div>
</div>

View File

@@ -60,7 +60,7 @@ export default {
},
toggleGroup() {
this.collapsed = !this.collapsed;
sessionStorage.setItem(`gatus:endpoint-group:${this.name}:collapsed`, this.collapsed);
localStorage.setItem(`gatus:endpoint-group:${this.name}:collapsed`, this.collapsed);
},
showTooltip(result, event) {
this.$emit('showTooltip', result, event);
@@ -80,7 +80,7 @@ export default {
data() {
return {
unhealthyCount: 0,
collapsed: sessionStorage.getItem(`gatus:endpoint-group:${this.name}:collapsed`) === "true"
collapsed: localStorage.getItem(`gatus:endpoint-group:${this.name}:collapsed`) === "true"
}
}
}

View File

@@ -3,7 +3,7 @@
<div class="text-xs text-gray-600 rounded-xl py-1.5 px-1.5 dark:text-gray-200">
<ArrowPathIcon class="w-3"/>
</div>
<select class="text-center text-gray-500 text-xs dark:text-gray-200 dark:bg-gray-800 border-r border-l border-gray-300 dark:border-gray-500" id="refresh-rate" ref="refreshInterval" @change="handleChangeRefreshInterval">
<select class="text-center text-gray-500 text-xs dark:text-gray-200 dark:bg-gray-800 border-r border-l border-gray-300 dark:border-gray-500 pl-1" id="refresh-rate" ref="refreshInterval" @change="handleChangeRefreshInterval">
<option value="10" :selected="refreshInterval === 10">10s</option>
<option value="30" :selected="refreshInterval === 30">30s</option>
<option value="60" :selected="refreshInterval === 60">1m</option>
@@ -33,7 +33,7 @@ export default {
props: {},
methods: {
setRefreshInterval(seconds) {
sessionStorage.setItem('gatus:refresh-interval', seconds);
localStorage.setItem('gatus:refresh-interval', seconds);
let that = this;
this.refreshIntervalHandler = setInterval(function () {
that.refreshData();
@@ -78,7 +78,7 @@ export default {
},
data() {
return {
refreshInterval: sessionStorage.getItem('gatus:refresh-interval') < 10 ? 300 : parseInt(sessionStorage.getItem('gatus:refresh-interval')),
refreshInterval: localStorage.getItem('gatus:refresh-interval') < 10 ? 300 : parseInt(localStorage.getItem('gatus:refresh-interval')),
refreshIntervalHandler: 0,
darkMode: true
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long