mirror of
https://github.com/TwiN/gatus.git
synced 2026-09-26 17:54:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7bf2c194 | ||
|
|
db8535c3f3 | ||
|
|
fdd00e7851 | ||
|
|
2a94f76244 | ||
|
|
f2c5f5911c | ||
|
|
9d151fcdb4 | ||
|
|
4397dcb5fc | ||
|
|
739e6c75a6 | ||
|
|
dcfdfd423e | ||
|
|
28339684bf | ||
|
|
519500508a | ||
|
|
76a84031c2 | ||
|
|
b88d0b1c34 | ||
|
|
e0ab35e86a | ||
|
|
7efe7429dd | ||
|
|
4393a49900 | ||
|
|
241956b28c | ||
|
|
a4bc3c4dfe | ||
|
|
7cf2b427c9 | ||
|
|
dc9cddfd77 | ||
|
|
f93cebe715 | ||
|
|
f54c45e20e | ||
|
|
cacfbc0185 | ||
|
|
922638e071 | ||
|
|
979d467e36 | ||
|
|
ceb2c7884f | ||
|
|
ae750aa367 | ||
|
|
5aa83ee274 | ||
|
|
1e431c797a | ||
|
|
143a093e20 | ||
|
|
1eba430797 | ||
|
|
6299b630ce | ||
|
|
1fcc6c0cc0 | ||
|
|
408a46f2af | ||
|
|
3269e96f49 | ||
|
|
08742e4af3 | ||
|
|
2a623a59d3 | ||
|
|
3d1b4e566d | ||
|
|
6cbc59b0e8 | ||
|
|
1a7aeb5b35 | ||
|
|
228cd4d1fb | ||
|
|
bdad56e205 | ||
|
|
911902353f | ||
|
|
f76be6df92 | ||
|
|
dd6c4142fb | ||
|
|
1e82d2f07d | ||
|
|
3c246f0c69 |
@@ -32,7 +32,7 @@ endpoints:
|
|||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
query-type: "A"
|
query-type: "A"
|
||||||
conditions:
|
conditions:
|
||||||
- "[BODY] == 93.184.216.34"
|
- "[BODY] == 93.184.215.14"
|
||||||
- "[DNS_RCODE] == NOERROR"
|
- "[DNS_RCODE] == NOERROR"
|
||||||
|
|
||||||
- name: icmp-ping
|
- name: icmp-ping
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ endpoints:
|
|||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
query-type: "A"
|
query-type: "A"
|
||||||
conditions:
|
conditions:
|
||||||
- "[BODY] == 93.184.216.34"
|
- "[BODY] == 93.184.215.14"
|
||||||
- "[DNS_RCODE] == NOERROR"
|
- "[DNS_RCODE] == NOERROR"
|
||||||
|
|
||||||
- name: icmp-ping
|
- name: icmp-ping
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: 1.19
|
||||||
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: 1.21
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build binary to make sure it works
|
- name: Build binary to make sure it works
|
||||||
run: go build
|
run: go build
|
||||||
@@ -28,6 +28,7 @@ jobs:
|
|||||||
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
||||||
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||||
- name: Codecov
|
- name: Codecov
|
||||||
uses: codecov/codecov-action@v3.1.4
|
uses: codecov/codecov-action@v4.4.0
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ FROM golang:alpine as builder
|
|||||||
RUN apk --update add ca-certificates
|
RUN apk --update add ca-certificates
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
RUN go mod tidy
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gatus .
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gatus .
|
||||||
|
|
||||||
# Run Tests inside docker image if you don't have a configured go environment
|
# Run Tests inside docker image if you don't have a configured go environment
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
BINARY=gatus
|
BINARY=gatus
|
||||||
|
|
||||||
# Because there's a folder called "test", we need to make the target "test" phony
|
.PHONY: install
|
||||||
.PHONY: test
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -mod vendor -o $(BINARY) .
|
go build -v -o $(BINARY) .
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
run:
|
run:
|
||||||
GATUS_CONFIG_PATH=./config.yaml ./$(BINARY)
|
GATUS_CONFIG_PATH=./config.yaml ./$(BINARY)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm $(BINARY)
|
rm $(BINARY)
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
go test ./... -cover
|
go test ./... -cover
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
||||||
[](https://github.com/TwiN)
|
[](https://github.com/TwiN)
|
||||||
|
|
||||||
|
|
||||||
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
||||||
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
||||||
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
||||||
@@ -38,23 +39,26 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Why Gatus?](#why-gatus)
|
- [Why Gatus?](#why-gatus)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
|
- [Endpoints](#endpoints)
|
||||||
|
- [External Endpoints](#external-endpoints)
|
||||||
- [Conditions](#conditions)
|
- [Conditions](#conditions)
|
||||||
- [Placeholders](#placeholders)
|
- [Placeholders](#placeholders)
|
||||||
- [Functions](#functions)
|
- [Functions](#functions)
|
||||||
- [Storage](#storage)
|
- [Storage](#storage)
|
||||||
- [Client configuration](#client-configuration)
|
- [Client configuration](#client-configuration)
|
||||||
- [Alerting](#alerting)
|
- [Alerting](#alerting)
|
||||||
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
|
|
||||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||||
- [Configuring Email alerts](#configuring-email-alerts)
|
- [Configuring Email alerts](#configuring-email-alerts)
|
||||||
- [Configuring GitHub alerts](#configuring-github-alerts)
|
- [Configuring GitHub alerts](#configuring-github-alerts)
|
||||||
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
|
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
|
||||||
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
||||||
- [Configuring Gotify alerts](#configuring-gotify-alerts)
|
- [Configuring Gotify alerts](#configuring-gotify-alerts)
|
||||||
|
- [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts)
|
||||||
- [Configuring Matrix alerts](#configuring-matrix-alerts)
|
- [Configuring Matrix alerts](#configuring-matrix-alerts)
|
||||||
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
||||||
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
||||||
@@ -66,6 +70,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Configuring Teams alerts](#configuring-teams-alerts)
|
- [Configuring Teams alerts](#configuring-teams-alerts)
|
||||||
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
||||||
- [Configuring Twilio alerts](#configuring-twilio-alerts)
|
- [Configuring Twilio alerts](#configuring-twilio-alerts)
|
||||||
|
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
|
||||||
- [Configuring custom alerts](#configuring-custom-alerts)
|
- [Configuring custom alerts](#configuring-custom-alerts)
|
||||||
- [Setting a default alert](#setting-a-default-alert)
|
- [Setting a default alert](#setting-a-default-alert)
|
||||||
- [Maintenance](#maintenance)
|
- [Maintenance](#maintenance)
|
||||||
@@ -101,15 +106,18 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Endpoint groups](#endpoint-groups)
|
- [Endpoint groups](#endpoint-groups)
|
||||||
- [Exposing Gatus on a custom path](#exposing-gatus-on-a-custom-path)
|
- [Exposing Gatus on a custom path](#exposing-gatus-on-a-custom-path)
|
||||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||||
|
- [Configuring a startup delay](#configuring-a-startup-delay)
|
||||||
- [Keeping your configuration small](#keeping-your-configuration-small)
|
- [Keeping your configuration small](#keeping-your-configuration-small)
|
||||||
|
- [Proxy client configuration](#proxy-client-configuration)
|
||||||
- [Badges](#badges)
|
- [Badges](#badges)
|
||||||
- [Uptime](#uptime)
|
- [Uptime](#uptime)
|
||||||
- [Health](#health)
|
- [Health](#health)
|
||||||
|
- [Health (Shields.io)](#health-shieldsio)
|
||||||
- [Response time](#response-time)
|
- [Response time](#response-time)
|
||||||
|
- [How to change the color thresholds of the response time badge](#how-to-change-the-color-thresholds-of-the-response-time-badge)
|
||||||
- [API](#api)
|
- [API](#api)
|
||||||
- [Installing as binary](#installing-as-binary)
|
- [Installing as binary](#installing-as-binary)
|
||||||
- [High level design overview](#high-level-design-overview)
|
- [High level design overview](#high-level-design-overview)
|
||||||
- [Sponsors](#sponsors)
|
|
||||||
|
|
||||||
|
|
||||||
## Why Gatus?
|
## Why Gatus?
|
||||||
@@ -132,6 +140,7 @@ fixing the issue before they even know about it.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
The main features of Gatus are:
|
The main features of Gatus are:
|
||||||
|
|
||||||
- **Highly flexible health check conditions**: While checking the response status may be enough for some use cases, Gatus goes much further and allows you to add conditions on the response time, the response body and even the IP address.
|
- **Highly flexible health check conditions**: While checking the response status may be enough for some use cases, Gatus goes much further and allows you to add conditions on the response time, the response body and even the IP address.
|
||||||
- **Ability to use Gatus for user acceptance tests**: Thanks to the point above, you can leverage this application to create automated user acceptance tests.
|
- **Ability to use Gatus for user acceptance tests**: Thanks to the point above, you can leverage this application to create automated user acceptance tests.
|
||||||
- **Very easy to configure**: Not only is the configuration designed to be as readable as possible, it's also extremely easy to add a new service or a new endpoint to monitor.
|
- **Very easy to configure**: Not only is the configuration designed to be as readable as possible, it's also extremely easy to add a new service or a new endpoint to monitor.
|
||||||
@@ -194,66 +203,116 @@ 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.
|
- 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}`)
|
> 💡 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.
|
> 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).
|
If you want to test it locally, see [Docker](#docker).
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:-----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
||||||
|
| `debug` | Whether to enable debug logs. | `false` |
|
||||||
|
| `metrics` | Whether to expose metrics at `/metrics`. | `false` |
|
||||||
|
| `storage` | [Storage configuration](#storage). | `{}` |
|
||||||
|
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
||||||
|
| `endpoints` | [Endpoints configuration](#endpoints). | Required `[]` |
|
||||||
|
| `external-endpoints` | [External Endpoints configuration](#external-endpoints). | `[]` |
|
||||||
|
| `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.read-buffer-size` | Buffer size for reading requests from a connection. Also limit for the maximum header size. | `8192` |
|
||||||
|
| `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). | `{}` |
|
||||||
|
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
Endpoints are URLs, applications, or services that you want to monitor. Each endpoint has a list of conditions that are
|
||||||
|
evaluated on an interval that you define. If any condition fails, the endpoint is considered as unhealthy.
|
||||||
|
You can then configure alerts to be triggered when an endpoint is unhealthy once a certain threshold is reached.
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
||||||
| `debug` | Whether to enable debug logs. | `false` |
|
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
||||||
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
||||||
| `storage` | [Storage configuration](#storage) | `{}` |
|
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
||||||
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
||||||
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
| `endpoints[].url` | URL to send the request to. | Required `""` |
|
||||||
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
| `endpoints[].method` | Request method. | `GET` |
|
||||||
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
||||||
| `endpoints[].url` | URL to send the request to. | Required `""` |
|
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
|
||||||
| `endpoints[].method` | Request method. | `GET` |
|
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
||||||
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
| `endpoints[].body` | Request body. | `""` |
|
||||||
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
|
| `endpoints[].headers` | Request headers. | `{}` |
|
||||||
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
||||||
| `endpoints[].body` | Request body. | `""` |
|
| `endpoints[].dns.query-type` | Query type (e.g. MX). | `""` |
|
||||||
| `endpoints[].headers` | Request headers. | `{}` |
|
| `endpoints[].dns.query-name` | Query name (e.g. example.com). | `""` |
|
||||||
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
| `endpoints[].ssh` | Configuration for an endpoint of type SSH. <br />See [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh). | `""` |
|
||||||
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
|
| `endpoints[].ssh.username` | SSH username (e.g. example). | Required `""` |
|
||||||
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
| `endpoints[].ssh.password` | SSH password (e.g. password). | Required `""` |
|
||||||
| `endpoints[].ssh` | Configuration for an endpoint of type SSH. <br />See [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh). | `""` |
|
| `endpoints[].alerts` | List of all alerts for a given endpoint. <br />See [Alerting](#alerting). | `[]` |
|
||||||
| `endpoints[].ssh.username` | SSH username (e.g. example) | Required `""` |
|
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
||||||
| `endpoints[].ssh.password` | SSH password (e.g. password) | Required `""` |
|
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
||||||
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
|
| `endpoints[].ui.hide-conditions` | Whether to hide conditions from the results. Note that this only hides conditions from results evaluated from the moment this was enabled. | `false` |
|
||||||
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
|
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
|
||||||
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
|
||||||
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||||
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `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]` |
|
||||||
| `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. | `{}` |
|
### External Endpoints
|
||||||
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
|
Unlike regular endpoints, external endpoints are not monitored by Gatus, but they are instead pushed programmatically.
|
||||||
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
|
This allows you to monitor anything you want, even when what you want to check lives in an environment that would not normally be accessible by Gatus.
|
||||||
| `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]` |
|
For instance:
|
||||||
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
- You can create your own agent that lives in a private network and pushes the status of your services to a publicly-exposed Gatus instance
|
||||||
| `security` | [Security configuration](#security). | `{}` |
|
- You can monitor services that are not supported by Gatus
|
||||||
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
- You can implement your own monitoring system while using Gatus as the dashboard
|
||||||
| `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. | `{}` |
|
| Parameter | Description | Default |
|
||||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
|:-------------------------------|:-----------------------------------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `web.port` | Port to listen on. | `8080` |
|
| `external-endpoints` | List of endpoints to monitor. | `[]` |
|
||||||
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
|
| `external-endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
||||||
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
|
| `external-endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
||||||
| `ui` | UI configuration. | `{}` |
|
| `external-endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
||||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
| `external-endpoints[].token` | Bearer token required to push status to. | Required `""` |
|
||||||
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
|
| `external-endpoints[].alerts` | List of all alerts for a given endpoint. <br />See [Alerting](#alerting). | `[]` |
|
||||||
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
|
|
||||||
| `ui.logo` | URL to the logo to display. | `""` |
|
Example:
|
||||||
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
```yaml
|
||||||
| `ui.buttons` | List of buttons to display below the header. | `[]` |
|
external-endpoints:
|
||||||
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
|
- name: ext-ep-test
|
||||||
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
|
group: core
|
||||||
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
|
token: "potato"
|
||||||
|
alerts:
|
||||||
|
- type: discord
|
||||||
|
description: "healthcheck failed"
|
||||||
|
send-on-resolved: true
|
||||||
|
```
|
||||||
|
|
||||||
|
To push the status of an external endpoint, the request would have to look like this:
|
||||||
|
```
|
||||||
|
POST /api/v1/endpoints/{key}/external?success={success}
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
||||||
|
- Using the example configuration above, the key would be `core_ext-ep-test`.
|
||||||
|
- `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
|
||||||
|
|
||||||
|
You must also pass the token as a `Bearer` token in the `Authorization` header.
|
||||||
|
|
||||||
|
|
||||||
### Conditions
|
### Conditions
|
||||||
@@ -346,31 +405,42 @@ See [examples/docker-compose-postgres-storage](.examples/docker-compose-postgres
|
|||||||
In order to support a wide range of environments, each monitored endpoint has a unique configuration for
|
In order to support a wide range of environments, each monitored endpoint has a unique configuration for
|
||||||
the client used to send the request.
|
the client used to send the request.
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------|:---------------------------------------------------------------------------|:----------------|
|
|:---------------------------------------|:----------------------------------------------------------------------------|:----------------|
|
||||||
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
||||||
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
||||||
| `client.timeout` | Duration before timing out. | `10s` |
|
| `client.timeout` | Duration before timing out. | `10s` |
|
||||||
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
|
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
|
||||||
| `client.oauth2` | OAuth2 client configuration. | `{}` |
|
| `client.oauth2` | OAuth2 client configuration. | `{}` |
|
||||||
| `client.oauth2.token-url` | The token endpoint URL | required `""` |
|
| `client.oauth2.token-url` | The token endpoint URL | required `""` |
|
||||||
| `client.oauth2.client-id` | The client id which should be used for the `Client credentials flow` | required `""` |
|
| `client.oauth2.client-id` | The client id which should be used for the `Client credentials flow` | required `""` |
|
||||||
| `client.oauth2.client-secret` | The client secret which should be used for the `Client credentials flow` | required `""` |
|
| `client.oauth2.client-secret` | The client secret which should be used for the `Client credentials flow` | required `""` |
|
||||||
| `client.oauth2.scopes[]` | A list of `scopes` which should be used for the `Client credentials flow`. | required `[""]` |
|
| `client.oauth2.scopes[]` | A list of `scopes` which should be used for the `Client credentials flow`. | required `[""]` |
|
||||||
|
| `client.proxy-url` | The URL of the proxy to use for the client | `""` |
|
||||||
|
| `client.identity-aware-proxy` | Google Identity-Aware-Proxy client configuration. | `{}` |
|
||||||
|
| `client.identity-aware-proxy.audience` | The Identity-Aware-Proxy audience. (client-id of the IAP oauth2 credential) | required `""` |
|
||||||
|
| `client.tls.certificate-file` | Path to a client certificate (in PEM format) for mTLS configurations. | `""` |
|
||||||
|
| `client.tls.private-key-file` | Path to a client private key (in PEM format) for mTLS configurations. | `""` |
|
||||||
|
| `client.tls.renegotiation` | Type of renegotiation support to provide. (`never`, `freely`, `once`). | `"never"` |
|
||||||
|
| `client.network` | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`). | `"ip"` |
|
||||||
|
|
||||||
|
|
||||||
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
|
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
|
||||||
in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
|
> in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
|
||||||
|
|
||||||
This default configuration is as follows:
|
This default configuration is as follows:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
client:
|
client:
|
||||||
insecure: false
|
insecure: false
|
||||||
ignore-redirect: false
|
ignore-redirect: false
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that this configuration is only available under `endpoints[]`, `alerting.mattermost` and `alerting.custom`.
|
Note that this configuration is only available under `endpoints[]`, `alerting.mattermost` and `alerting.custom`.
|
||||||
|
|
||||||
Here's an example with the client configuration under `endpoints[]`:
|
Here's an example with the client configuration under `endpoints[]`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@@ -384,6 +454,7 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
This example shows how you can specify a custom DNS resolver:
|
This example shows how you can specify a custom DNS resolver:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: with-custom-dns-resolver
|
- name: with-custom-dns-resolver
|
||||||
@@ -395,6 +466,7 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
This example shows how you can use the `client.oauth2` configuration to query a backend API with `Bearer token`:
|
This example shows how you can use the `client.oauth2` configuration to query a backend API with `Bearer token`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: with-custom-oauth2
|
- name: with-custom-oauth2
|
||||||
@@ -409,46 +481,103 @@ endpoints:
|
|||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This example shows how you can use the `client.identity-aware-proxy` configuration to query a backend API with `Bearer token` using Google Identity-Aware-Proxy:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: with-custom-iap
|
||||||
|
url: "https://my.iap.protected.app/health"
|
||||||
|
client:
|
||||||
|
identity-aware-proxy:
|
||||||
|
audience: "XXXXXXXX-XXXXXXXXXXXX.apps.googleusercontent.com"
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
```
|
||||||
|
|
||||||
|
> 📝 Note that Gatus will use the [gcloud default credentials](https://cloud.google.com/docs/authentication/application-default-credentials) within its environment to generate the token.
|
||||||
|
|
||||||
|
This example shows you how you cna use the `client.tls` configuration to perform an mTLS query to a backend API:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://your.mtls.protected.app/health"
|
||||||
|
client:
|
||||||
|
tls:
|
||||||
|
certificate-file: /path/to/user_cert.pem
|
||||||
|
private-key-file: /path/to/user_key.pem
|
||||||
|
renegotiation: once
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
```
|
||||||
|
|
||||||
|
> 📝 Note that if running in a container, you must volume mount the certificate and key into the container.
|
||||||
|
|
||||||
### Alerting
|
### Alerting
|
||||||
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
||||||
individual endpoints with configurable descriptions and thresholds.
|
individual endpoints with configurable descriptions and thresholds.
|
||||||
|
|
||||||
> 📝 If an alerting provider is not properly configured, all alerts configured with the provider's type will be
|
Alerts are configured at the endpoint level like so:
|
||||||
ignored.
|
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------|:--------|
|
|:-----------------------------|:-------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.custom` | Configuration for custom actions on failure or alerts. <br />See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` |
|
| `alerts` | List of all alerts for a given endpoint. | `[]` |
|
||||||
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
|
| `alerts[].type` | Type of alert. <br />See table below for all valid types. | Required `""` |
|
||||||
| `alerting.email` | Configuration for alerts of type `email`. <br />See [Configuring Email alerts](#configuring-email-alerts). | `{}` |
|
| `alerts[].enabled` | Whether to enable the alert. | `true` |
|
||||||
| `alerting.github` | Configuration for alerts of type `github`. <br />See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` |
|
| `alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
||||||
| `alerting.gitlab` | Configuration for alerts of type `gitlab`. <br />See [Configuring GitLab alerts](#configuring-gitlab-alerts). | `{}` |
|
| `alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
||||||
| `alerting.googlechat` | Configuration for alerts of type `googlechat`. <br />See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` |
|
| `alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
||||||
| `alerting.gotify` | Configuration for alerts of type `gotify`. <br />See [Configuring Gotify alerts](#configuring-gotify-alerts). | `{}` |
|
| `alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||||
| `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). | `{}` |
|
Here's an example of what an alert configuration might look like at the endpoint level:
|
||||||
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
|
```yaml
|
||||||
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
|
endpoints:
|
||||||
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
- name: example
|
||||||
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
|
url: "https://example.org"
|
||||||
| `alerting.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
|
conditions:
|
||||||
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
- "[STATUS] == 200"
|
||||||
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
|
alerts:
|
||||||
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
|
- type: slack
|
||||||
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
|
description: "healthcheck failed"
|
||||||
|
send-on-resolved: true
|
||||||
|
```
|
||||||
|
|
||||||
|
> 📝 If an alerting provider is not properly configured, all alerts configured with the provider's type will be
|
||||||
|
> ignored.
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:--------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|:--------|
|
||||||
|
| `alerting.custom` | Configuration for custom actions on failure or alerts. <br />See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` |
|
||||||
|
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
|
||||||
|
| `alerting.email` | Configuration for alerts of type `email`. <br />See [Configuring Email alerts](#configuring-email-alerts). | `{}` |
|
||||||
|
| `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.jetbrainsspace` | Configuration for alerts of type `jetbrainsspace`. <br />See [Configuring JetBrains Space alerts](#configuring-jetbrains-space-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). | `{}` |
|
||||||
|
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
|
||||||
|
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
||||||
|
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
|
||||||
|
| `alerting.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
|
||||||
|
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
||||||
|
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
|
||||||
|
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
|
||||||
|
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Discord alerts
|
#### Configuring Discord alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:------------------------------------|
|
||||||
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
||||||
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
|
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
|
||||||
| `alerting.discord.title` | Title of the notification | `":helmet_with_white_cross: Gatus"` |
|
| `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.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` | 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 | `""` |
|
| `alerting.discord.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
| `alerting.discord.overrides[].webhook-url` | Discord Webhook URL | `""` |
|
| `alerting.discord.overrides[].webhook-url` | Discord Webhook URL | `""` |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -568,17 +697,18 @@ endpoints:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
#### Configuring GitLab alerts
|
#### Configuring GitLab alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------|:----------------------------------------------------------------------------------------------------------------|:--------------|
|
|:------------------------------------|:--------------------------------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.gitlab` | Configuration for alerts of type `gitlab` | `{}` |
|
| `alerting.gitlab` | Configuration for alerts of type `gitlab` | `{}` |
|
||||||
| `alerting.gitlab.webhook-url` | GitLab alert webhook URL (e.g. `https://gitlab.com/hlidotbe/example/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json`) | Required `""` |
|
| `alerting.gitlab.webhook-url` | GitLab alert webhook URL (e.g. `https://gitlab.com/yourusername/example/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json`) | Required `""` |
|
||||||
| `alerting.gitlab.authorization-key` | Personal access token to use for authentication. <br />Must have at least RW on issues and RO on metadata. | Required `""` |
|
| `alerting.gitlab.authorization-key` | GitLab alert authorization key. | Required `""` |
|
||||||
| `alerting.gitlab.severity` | Override default severity (critical), can be one of `critical, high, medium, low, info, unknown` | `""` |
|
| `alerting.gitlab.severity` | Override default severity (critical), can be one of `critical, high, medium, low, info, unknown` | `""` |
|
||||||
| `alerting.gitlab.monitoring-tool` | Override the monitoring tool name (gatus) | `"gatus"` |
|
| `alerting.gitlab.monitoring-tool` | Override the monitoring tool name (gatus) | `"gatus"` |
|
||||||
| `alerting.gitlab.environment-name` | Set gitlab environment's name. Required to display alerts on a dashboard. | `""` |
|
| `alerting.gitlab.environment-name` | Set gitlab environment's name. Required to display alerts on a dashboard. | `""` |
|
||||||
| `alerting.gitlab.service` | Override endpoint displayname | `""` |
|
| `alerting.gitlab.service` | Override endpoint display name | `""` |
|
||||||
| `alerting.gitlab.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
| `alerting.gitlab.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||||
|
|
||||||
The GitLab alerting provider creates an alert prefixed with `alert(gatus):` and suffixed with the endpoint's display
|
The GitLab alerting provider creates an alert prefixed with `alert(gatus):` and suffixed with the endpoint's display
|
||||||
name for each alert. If `send-on-resolved` is set to `true` on the endpoint alert, the alert will be automatically
|
name for each alert. If `send-on-resolved` is set to `true` on the endpoint alert, the alert will be automatically
|
||||||
@@ -642,14 +772,14 @@ endpoints:
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring Gotify alerts
|
#### Configuring Gotify alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:-----------------------|
|
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:----------------------|
|
||||||
| `alerting.gotify` | Configuration for alerts of type `gotify` | `{}` |
|
| `alerting.gotify` | Configuration for alerts of type `gotify` | `{}` |
|
||||||
| `alerting.gotify.server-url` | Gotify server URL | Required `""` |
|
| `alerting.gotify.server-url` | Gotify server URL | Required `""` |
|
||||||
| `alerting.gotify.token` | Token that is used for authentication. | 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.priority` | Priority of the alert according to Gotify standards. | `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>"` |
|
||||||
| `alerting.gotify.title` | Title of the notification | `"Gatus: <endpoint>"` |
|
| `alerting.gotify.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -676,6 +806,41 @@ Here's an example of what the notifications look like:
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
#### Configuring JetBrains Space alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:---------------------------------------------------|:--------------------------------------------------------------------------------------------|:-----------------------|
|
||||||
|
| `alerting.jetbrainsspace` | Configuration for alerts of type `jetbrainsspace` | `{}` |
|
||||||
|
| `alerting.jetbrainsspace.project` | JetBrains Space project name | Required `""` |
|
||||||
|
| `alerting.jetbrainsspace.channel-id` | JetBrains Space Chat Channel ID | Required `""` |
|
||||||
|
| `alerting.jetbrainsspace.token` | Token that is used for authentication. | Required `""` |
|
||||||
|
| `alerting.jetbrainsspace.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
| `alerting.jetbrainsspace.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
|
| `alerting.jetbrainsspace.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
jetbrainsspace:
|
||||||
|
project: myproject
|
||||||
|
channel-id: ABCDE12345
|
||||||
|
token: "**************"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
alerts:
|
||||||
|
- type: jetbrainsspace
|
||||||
|
description: "healthcheck failed"
|
||||||
|
send-on-resolved: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's an example of what the notifications look like:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
#### Configuring Matrix alerts
|
#### Configuring Matrix alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------|
|
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------|
|
||||||
@@ -928,6 +1093,7 @@ endpoints:
|
|||||||
description: "healthcheck failed"
|
description: "healthcheck failed"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Slack alerts
|
#### Configuring Slack alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
@@ -967,14 +1133,15 @@ Here's an example of what the notifications look like:
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring Teams alerts
|
#### Configuring Teams alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------------|
|
||||||
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
||||||
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
|
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
|
||||||
| `alerting.teams.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
| `alerting.teams.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
| `alerting.teams.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
| `alerting.teams.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
| `alerting.teams.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
| `alerting.teams.title` | Title of the notification | `"🚨 Gatus"` |
|
||||||
| `alerting.teams.overrides[].webhook-url` | Teams Webhook URL | `""` |
|
| `alerting.teams.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
|
| `alerting.teams.overrides[].webhook-url` | Teams Webhook URL | `""` |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -1338,7 +1505,7 @@ security:
|
|||||||
```
|
```
|
||||||
|
|
||||||
> ⚠ Make sure to carefully select to cost of the bcrypt hash. The higher the cost, the longer it takes to compute the hash,
|
> ⚠ Make sure to carefully select to cost of the bcrypt hash. The higher the cost, the longer it takes to compute the hash,
|
||||||
and basic auth verifies the password against the hash on every request. As of 2023-01-06, I suggest a cost of 9.
|
> and basic auth verifies the password against the hash on every request. As of 2023-01-06, I suggest a cost of 9.
|
||||||
|
|
||||||
|
|
||||||
#### OIDC
|
#### OIDC
|
||||||
@@ -1366,6 +1533,7 @@ security:
|
|||||||
|
|
||||||
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
|
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
|
||||||
|
|
||||||
|
|
||||||
### TLS Encryption
|
### TLS Encryption
|
||||||
Gatus supports basic encryption with TLS. To enable this, certificate files in PEM format have to be provided.
|
Gatus supports basic encryption with TLS. To enable this, certificate files in PEM format have to be provided.
|
||||||
|
|
||||||
@@ -1378,6 +1546,7 @@ web:
|
|||||||
private-key-file: "private.key"
|
private-key-file: "private.key"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Metrics
|
### Metrics
|
||||||
To enable metrics, you must set `metrics` to `true`. Doing so will expose Prometheus-friendly metrics at the `/metrics`
|
To enable metrics, you must set `metrics` to `true`. Doing so will expose Prometheus-friendly metrics at the `/metrics`
|
||||||
endpoint on the same port your application is configured to run on (`web.port`).
|
endpoint on the same port your application is configured to run on (`web.port`).
|
||||||
@@ -1402,7 +1571,7 @@ See [examples/docker-compose-grafana-prometheus](.examples/docker-compose-grafan
|
|||||||
| `connectivity.checker.interval` | Interval at which to validate connectivity | `1m` |
|
| `connectivity.checker.interval` | Interval at which to validate connectivity | `1m` |
|
||||||
|
|
||||||
While Gatus is used to monitor other services, it is possible for Gatus itself to lose connectivity to the internet.
|
While Gatus is used to monitor other services, it is possible for Gatus itself to lose connectivity to the internet.
|
||||||
In order to prevent Gatus from reporting endpoints as unhealthy when Gatus itself is unhealthy, you may configure
|
In order to prevent Gatus from reporting endpoints as unhealthy when Gatus itself is unhealthy, you may configure
|
||||||
Gatus to periodically check for internet connectivity.
|
Gatus to periodically check for internet connectivity.
|
||||||
|
|
||||||
All endpoint executions are skipped while the connectivity checker deems connectivity to be down.
|
All endpoint executions are skipped while the connectivity checker deems connectivity to be down.
|
||||||
@@ -1420,7 +1589,7 @@ This feature allows you to retrieve endpoint statuses from a remote Gatus instan
|
|||||||
|
|
||||||
There are two main use cases for this:
|
There are two main use cases for this:
|
||||||
- You have multiple Gatus instances running on different machines, and you wish to visually expose the statuses through a single dashboard
|
- You have multiple Gatus instances running on different machines, and you wish to visually expose the statuses through a single dashboard
|
||||||
- You have one or more Gatus instances that are not publicly accessible (e.g. behind a firewall), and you wish to retrieve
|
- You have one or more Gatus instances that are not publicly accessible (e.g. behind a firewall), and you wish to retrieve
|
||||||
|
|
||||||
This is an experimental feature. It may be removed or updated in a breaking manner at any time. Furthermore,
|
This is an experimental feature. It may be removed or updated in a breaking manner at any time. Furthermore,
|
||||||
there are known issues with this feature. If you'd like to provide some feedback, please write a comment in [#64](https://github.com/TwiN/gatus/issues/64).
|
there are known issues with this feature. If you'd like to provide some feedback, please write a comment in [#64](https://github.com/TwiN/gatus/issues/64).
|
||||||
@@ -1480,7 +1649,7 @@ helm repo add minicloudlabs https://minicloudlabs.github.io/helm-charts
|
|||||||
```
|
```
|
||||||
|
|
||||||
To get more details, please check [chart's configuration](https://github.com/minicloudlabs/helm-charts/tree/main/charts/gatus#configuration)
|
To get more details, please check [chart's configuration](https://github.com/minicloudlabs/helm-charts/tree/main/charts/gatus#configuration)
|
||||||
and [helmfile example](https://github.com/minicloudlabs/helm-charts/tree/main/charts/gatus#helmfileyaml-example)
|
and [helm file example](https://github.com/minicloudlabs/helm-charts/tree/main/charts/gatus#helmfileyaml-example)
|
||||||
|
|
||||||
|
|
||||||
### Terraform
|
### Terraform
|
||||||
@@ -1489,7 +1658,7 @@ Gatus can be deployed on Terraform by using the following module: [terraform-kub
|
|||||||
|
|
||||||
## Running the tests
|
## Running the tests
|
||||||
```console
|
```console
|
||||||
go test ./... -mod vendor
|
go test -v ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -1586,8 +1755,9 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
|
|||||||
This works for applications such as databases (Postgres, MySQL, etc.) and caches (Redis, Memcached, etc.).
|
This works for applications such as databases (Postgres, MySQL, etc.) and caches (Redis, Memcached, etc.).
|
||||||
|
|
||||||
> 📝 `[CONNECTED] == true` does not guarantee that the endpoint itself is healthy - it only guarantees that there's
|
> 📝 `[CONNECTED] == true` does not guarantee that the endpoint itself is healthy - it only guarantees that there's
|
||||||
something at the given address listening to the given port, and that a connection to that address was successfully
|
> something at the given address listening to the given port, and that a connection to that address was successfully
|
||||||
established.
|
> established.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring a UDP endpoint
|
### Monitoring a UDP endpoint
|
||||||
By prefixing `endpoints[].url` with `udp:\\`, you can monitor UDP endpoints at a very basic level:
|
By prefixing `endpoints[].url` with `udp:\\`, you can monitor UDP endpoints at a very basic level:
|
||||||
@@ -1605,6 +1775,7 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
|
|||||||
|
|
||||||
This works for UDP based application.
|
This works for UDP based application.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring a SCTP endpoint
|
### Monitoring a SCTP endpoint
|
||||||
By prefixing `endpoints[].url` with `sctp:\\`, you can monitor Stream Control Transmission Protocol (SCTP) endpoints at a very basic level:
|
By prefixing `endpoints[].url` with `sctp:\\`, you can monitor Stream Control Transmission Protocol (SCTP) endpoints at a very basic level:
|
||||||
|
|
||||||
@@ -1621,6 +1792,7 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
|
|||||||
|
|
||||||
This works for SCTP based application.
|
This works for SCTP based application.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring a WebSocket endpoint
|
### Monitoring a WebSocket endpoint
|
||||||
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level:
|
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level:
|
||||||
|
|
||||||
@@ -1637,6 +1809,7 @@ endpoints:
|
|||||||
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
|
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
|
||||||
shows whether the connection was successfully established.
|
shows whether the connection was successfully established.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring an endpoint using ICMP
|
### Monitoring an endpoint using ICMP
|
||||||
By prefixing `endpoints[].url` with `icmp:\\`, you can monitor endpoints at a very basic level using ICMP, or more
|
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":
|
commonly known as "ping" or "echo":
|
||||||
@@ -1655,6 +1828,7 @@ You can specify a domain prefixed by `icmp://`, or an IP address prefixed by `ic
|
|||||||
If you run Gatus on Linux, please read the Linux section on https://github.com/prometheus-community/pro-bing#linux
|
If you run Gatus on Linux, please read the Linux section on https://github.com/prometheus-community/pro-bing#linux
|
||||||
if you encounter any problems.
|
if you encounter any problems.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring an endpoint using DNS queries
|
### Monitoring an endpoint using DNS queries
|
||||||
Defining a `dns` configuration in an endpoint will automatically mark said endpoint as an endpoint of type DNS:
|
Defining a `dns` configuration in an endpoint will automatically mark said endpoint as an endpoint of type DNS:
|
||||||
```yaml
|
```yaml
|
||||||
@@ -1665,7 +1839,7 @@ endpoints:
|
|||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
query-type: "A"
|
query-type: "A"
|
||||||
conditions:
|
conditions:
|
||||||
- "[BODY] == 93.184.216.34"
|
- "[BODY] == 93.184.215.14"
|
||||||
- "[DNS_RCODE] == NOERROR"
|
- "[DNS_RCODE] == NOERROR"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1674,6 +1848,7 @@ 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
|
- The placeholder `[DNS_RCODE]` resolves to the name associated to the response code returned by the query, such as
|
||||||
`NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, etc.
|
`NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, etc.
|
||||||
|
|
||||||
|
|
||||||
### Monitoring an endpoint using SSH
|
### Monitoring an endpoint using SSH
|
||||||
You can monitor endpoints using SSH by prefixing `endpoints[].url` with `ssh:\\`:
|
You can monitor endpoints using SSH by prefixing `endpoints[].url` with `ssh:\\`:
|
||||||
```yaml
|
```yaml
|
||||||
@@ -1683,7 +1858,7 @@ endpoints:
|
|||||||
ssh:
|
ssh:
|
||||||
username: "username"
|
username: "username"
|
||||||
password: "password"
|
password: "password"
|
||||||
body: |
|
body: |
|
||||||
{
|
{
|
||||||
"command": "uptime"
|
"command": "uptime"
|
||||||
}
|
}
|
||||||
@@ -1697,6 +1872,7 @@ The following placeholders are supported for endpoints of type SSH:
|
|||||||
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
|
- `[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)
|
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)
|
||||||
|
|
||||||
|
|
||||||
### Monitoring an endpoint using STARTTLS
|
### 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
|
If you have an email server that you want to ensure there are no problems with, monitoring it through STARTTLS
|
||||||
will serve as a good initial indicator:
|
will serve as a good initial indicator:
|
||||||
@@ -1712,6 +1888,7 @@ endpoints:
|
|||||||
- "[CERTIFICATE_EXPIRATION] > 48h"
|
- "[CERTIFICATE_EXPIRATION] > 48h"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Monitoring an endpoint using TLS
|
### Monitoring an endpoint using TLS
|
||||||
Monitoring endpoints using SSL/TLS encryption, such as LDAP over TLS, can help detect certificate expiration:
|
Monitoring endpoints using SSL/TLS encryption, such as LDAP over TLS, can help detect certificate expiration:
|
||||||
```yaml
|
```yaml
|
||||||
@@ -1741,9 +1918,9 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
> ⚠ The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois)
|
> ⚠ The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois)
|
||||||
and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`).
|
> and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`).
|
||||||
To prevent the WHOIS service from throttling your IP address if you send too many requests, Gatus will prevent you from
|
> To prevent the WHOIS service from throttling your IP address if you send too many requests, Gatus will prevent you from
|
||||||
using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`.
|
> using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`.
|
||||||
|
|
||||||
|
|
||||||
### disable-monitoring-lock
|
### disable-monitoring-lock
|
||||||
@@ -1850,6 +2027,10 @@ web:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Configuring a startup delay
|
||||||
|
If, for any reason, you need Gatus to wait for a given amount of time before monitoring the endpoints on application start, you can use the `GATUS_DELAY_START_SECONDS` environment variable to make Gatus sleep on startup.
|
||||||
|
|
||||||
|
|
||||||
### Keeping your configuration small
|
### Keeping your configuration small
|
||||||
While not specific to Gatus, you can leverage YAML anchors to create a default configuration.
|
While not specific to Gatus, you can leverage YAML anchors to create a default configuration.
|
||||||
If you have a large configuration file, this should help you keep things clean.
|
If you have a large configuration file, this should help you keep things clean.
|
||||||
@@ -1873,7 +2054,7 @@ endpoints:
|
|||||||
url: "https://example.org"
|
url: "https://example.org"
|
||||||
|
|
||||||
- name: anchor-example-2
|
- name: anchor-example-2
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
group: example # This will override the group defined in &defaults
|
group: example # This will override the group defined in &defaults
|
||||||
url: "https://example.com"
|
url: "https://example.com"
|
||||||
|
|
||||||
@@ -1887,6 +2068,45 @@ endpoints:
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
### Proxy client configuration
|
||||||
|
|
||||||
|
You can configure a proxy for the client to use by setting the `proxy-url` parameter in the client configuration.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
client:
|
||||||
|
proxy-url: http://proxy.example.com:8080
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proxy client configuration
|
||||||
|
|
||||||
|
You can configure a proxy for the client to use by setting the `proxy-url` parameter in the client configuration.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
client:
|
||||||
|
proxy-url: http://proxy.example.com:8080
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to fix 431 Request Header Fields Too Large error
|
||||||
|
Depending on where your environment is deployed and what kind of middleware or reverse proxy sits in front of Gatus,
|
||||||
|
you may run into this issue. This could be because the request headers are too large, e.g. big cookies.
|
||||||
|
|
||||||
|
By default, `web.read-buffer-size` is set to `8192`, but increasing this value like so will increase the read buffer size:
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
read-buffer-size: 32768
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Badges
|
### Badges
|
||||||
#### Uptime
|
#### Uptime
|
||||||

|

|
||||||
@@ -1938,6 +2158,25 @@ https://example.com/api/v1/endpoints/core_frontend/health/badge.svg
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Health (Shields.io)
|
||||||
|

|
||||||
|
|
||||||
|
The path to generate a badge is the following:
|
||||||
|
```
|
||||||
|
/api/v1/endpoints/{key}/health/badge.shields
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
||||||
|
|
||||||
|
For instance, if you want the current status of the endpoint `frontend` in the group `core`,
|
||||||
|
the URL would look like this:
|
||||||
|
```
|
||||||
|
https://example.com/api/v1/endpoints/core_frontend/health/badge.shields
|
||||||
|
```
|
||||||
|
|
||||||
|
See more information about the Shields.io badge endpoint [here](https://shields.io/badges/endpoint-badge).
|
||||||
|
|
||||||
|
|
||||||
#### Response time
|
#### Response time
|
||||||

|

|
||||||

|

|
||||||
@@ -1952,10 +2191,10 @@ Where:
|
|||||||
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
||||||
|
|
||||||
|
|
||||||
##### How to change the color thresholds of the response time badge
|
##### How to change the color thresholds of the response time badge
|
||||||
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
|
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
|
||||||
The values in the array correspond to the levels [Awesome, Great, Good, Passable, Bad]
|
The values in the array correspond to the levels [Awesome, Great, Good, Passable, Bad]
|
||||||
All five values must be given in milliseconds (ms).
|
All five values must be given in milliseconds (ms).
|
||||||
|
|
||||||
```
|
```
|
||||||
endpoints:
|
endpoints:
|
||||||
@@ -1999,5 +2238,6 @@ You can download Gatus as a binary using the following command:
|
|||||||
go install github.com/TwiN/gatus/v5@latest
|
go install github.com/TwiN/gatus/v5@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### High level design overview
|
### High level design overview
|
||||||

|

|
||||||
|
|||||||
+24
-7
@@ -1,7 +1,10 @@
|
|||||||
package alert
|
package alert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@ var (
|
|||||||
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
|
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Alert is a core.Endpoint's alert configuration
|
// Alert is a endpoint.Endpoint's alert configuration
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
// Type of alert (required)
|
// Type of alert (required)
|
||||||
Type Type `yaml:"type"`
|
Type Type `yaml:"type"`
|
||||||
@@ -26,6 +29,9 @@ type Alert struct {
|
|||||||
// FailureThreshold is the number of failures in a row needed before triggering the alert
|
// FailureThreshold is the number of failures in a row needed before triggering the alert
|
||||||
FailureThreshold int `yaml:"failure-threshold"`
|
FailureThreshold int `yaml:"failure-threshold"`
|
||||||
|
|
||||||
|
// SuccessThreshold defines how many successful executions must happen in a row before an ongoing incident is marked as resolved
|
||||||
|
SuccessThreshold int `yaml:"success-threshold"`
|
||||||
|
|
||||||
// Description of the alert. Will be included in the alert sent.
|
// Description of the alert. Will be included in the alert sent.
|
||||||
//
|
//
|
||||||
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
||||||
@@ -38,9 +44,6 @@ type Alert struct {
|
|||||||
// or not for provider.ParseWithDefaultAlert to work. Use Alert.IsSendingOnResolved() for a non-pointer
|
// or not for provider.ParseWithDefaultAlert to work. Use Alert.IsSendingOnResolved() for a non-pointer
|
||||||
SendOnResolved *bool `yaml:"send-on-resolved"`
|
SendOnResolved *bool `yaml:"send-on-resolved"`
|
||||||
|
|
||||||
// SuccessThreshold defines how many successful executions must happen in a row before an ongoing incident is marked as resolved
|
|
||||||
SuccessThreshold int `yaml:"success-threshold"`
|
|
||||||
|
|
||||||
// ResolveKey is an optional field that is used by some providers (i.e. PagerDuty's dedup_key) to resolve
|
// ResolveKey is an optional field that is used by some providers (i.e. PagerDuty's dedup_key) to resolve
|
||||||
// ongoing/triggered incidents
|
// ongoing/triggered incidents
|
||||||
ResolveKey string `yaml:"-"`
|
ResolveKey string `yaml:"-"`
|
||||||
@@ -71,7 +74,7 @@ func (alert *Alert) ValidateAndSetDefaults() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDescription retrieves the description of the alert
|
// GetDescription retrieves the description of the alert
|
||||||
func (alert Alert) GetDescription() string {
|
func (alert *Alert) GetDescription() string {
|
||||||
if alert.Description == nil {
|
if alert.Description == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -80,7 +83,7 @@ func (alert Alert) GetDescription() string {
|
|||||||
|
|
||||||
// IsEnabled returns whether an alert is enabled or not
|
// IsEnabled returns whether an alert is enabled or not
|
||||||
// Returns true if not set
|
// Returns true if not set
|
||||||
func (alert Alert) IsEnabled() bool {
|
func (alert *Alert) IsEnabled() bool {
|
||||||
if alert.Enabled == nil {
|
if alert.Enabled == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -88,9 +91,23 @@ func (alert Alert) IsEnabled() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsSendingOnResolved returns whether an alert is sending on resolve or not
|
// IsSendingOnResolved returns whether an alert is sending on resolve or not
|
||||||
func (alert Alert) IsSendingOnResolved() bool {
|
func (alert *Alert) IsSendingOnResolved() bool {
|
||||||
if alert.SendOnResolved == nil {
|
if alert.SendOnResolved == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return *alert.SendOnResolved
|
return *alert.SendOnResolved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checksum returns a checksum of the alert
|
||||||
|
// Used to determine which persisted triggered alert should be deleted on application start
|
||||||
|
func (alert *Alert) Checksum() string {
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write([]byte(string(alert.Type) + "_" +
|
||||||
|
strconv.FormatBool(alert.IsEnabled()) + "_" +
|
||||||
|
strconv.FormatBool(alert.IsSendingOnResolved()) + "_" +
|
||||||
|
strconv.Itoa(alert.SuccessThreshold) + "_" +
|
||||||
|
strconv.Itoa(alert.FailureThreshold) + "_" +
|
||||||
|
alert.GetDescription()),
|
||||||
|
)
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package alert
|
package alert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ func TestAlert_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.name, func(t *testing.T) {
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
if err := scenario.alert.ValidateAndSetDefaults(); err != scenario.expectedError {
|
if err := scenario.alert.ValidateAndSetDefaults(); !errors.Is(err, scenario.expectedError) {
|
||||||
t.Errorf("expected error %v, got %v", scenario.expectedError, err)
|
t.Errorf("expected error %v, got %v", scenario.expectedError, err)
|
||||||
}
|
}
|
||||||
if scenario.alert.SuccessThreshold != scenario.expectedSuccessThreshold {
|
if scenario.alert.SuccessThreshold != scenario.expectedSuccessThreshold {
|
||||||
@@ -52,34 +53,140 @@ func TestAlert_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlert_IsEnabled(t *testing.T) {
|
func TestAlert_IsEnabled(t *testing.T) {
|
||||||
if !(Alert{Enabled: nil}).IsEnabled() {
|
if !(&Alert{Enabled: nil}).IsEnabled() {
|
||||||
t.Error("alert.IsEnabled() should've returned true, because Enabled was set to nil")
|
t.Error("alert.IsEnabled() should've returned true, because Enabled was set to nil")
|
||||||
}
|
}
|
||||||
if value := false; (Alert{Enabled: &value}).IsEnabled() {
|
if value := false; (&Alert{Enabled: &value}).IsEnabled() {
|
||||||
t.Error("alert.IsEnabled() should've returned false, because Enabled was set to false")
|
t.Error("alert.IsEnabled() should've returned false, because Enabled was set to false")
|
||||||
}
|
}
|
||||||
if value := true; !(Alert{Enabled: &value}).IsEnabled() {
|
if value := true; !(&Alert{Enabled: &value}).IsEnabled() {
|
||||||
t.Error("alert.IsEnabled() should've returned true, because Enabled was set to true")
|
t.Error("alert.IsEnabled() should've returned true, because Enabled was set to true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlert_GetDescription(t *testing.T) {
|
func TestAlert_GetDescription(t *testing.T) {
|
||||||
if (Alert{Description: nil}).GetDescription() != "" {
|
if (&Alert{Description: nil}).GetDescription() != "" {
|
||||||
t.Error("alert.GetDescription() should've returned an empty string, because Description was set to nil")
|
t.Error("alert.GetDescription() should've returned an empty string, because Description was set to nil")
|
||||||
}
|
}
|
||||||
if value := "description"; (Alert{Description: &value}).GetDescription() != value {
|
if value := "description"; (&Alert{Description: &value}).GetDescription() != value {
|
||||||
t.Error("alert.GetDescription() should've returned false, because Description was set to 'description'")
|
t.Error("alert.GetDescription() should've returned false, because Description was set to 'description'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlert_IsSendingOnResolved(t *testing.T) {
|
func TestAlert_IsSendingOnResolved(t *testing.T) {
|
||||||
if (Alert{SendOnResolved: nil}).IsSendingOnResolved() {
|
if (&Alert{SendOnResolved: nil}).IsSendingOnResolved() {
|
||||||
t.Error("alert.IsSendingOnResolved() should've returned false, because SendOnResolved was set to nil")
|
t.Error("alert.IsSendingOnResolved() should've returned false, because SendOnResolved was set to nil")
|
||||||
}
|
}
|
||||||
if value := false; (Alert{SendOnResolved: &value}).IsSendingOnResolved() {
|
if value := false; (&Alert{SendOnResolved: &value}).IsSendingOnResolved() {
|
||||||
t.Error("alert.IsSendingOnResolved() should've returned false, because SendOnResolved was set to false")
|
t.Error("alert.IsSendingOnResolved() should've returned false, because SendOnResolved was set to false")
|
||||||
}
|
}
|
||||||
if value := true; !(Alert{SendOnResolved: &value}).IsSendingOnResolved() {
|
if value := true; !(&Alert{SendOnResolved: &value}).IsSendingOnResolved() {
|
||||||
t.Error("alert.IsSendingOnResolved() should've returned true, because SendOnResolved was set to true")
|
t.Error("alert.IsSendingOnResolved() should've returned true, because SendOnResolved was set to true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlert_Checksum(t *testing.T) {
|
||||||
|
description1, description2 := "a", "b"
|
||||||
|
yes, no := true, false
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
alert Alert
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "barebone",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
},
|
||||||
|
expected: "fed0580e44ed5701dbba73afa1f14b2c53ca5a7b8067a860441c212916057fe3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-1",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Description: &description1,
|
||||||
|
},
|
||||||
|
expected: "005f407ebe506e74a4aeb46f74c28b376debead7011e1b085da3840f72ba9707",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "3c2c4a9570cdc614006993c21f79a860a7f5afea10cf70d1a79d3c49342ef2c8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-enabled-false",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Enabled: &no,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "837945c2b4cd5e961db3e63e10c348d4f1c3446ba68cf5a48e35a1ae22cf0c22",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-enabled-true",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Enabled: &yes, // it defaults to true if not set, but just to make sure
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "3c2c4a9570cdc614006993c21f79a860a7f5afea10cf70d1a79d3c49342ef2c8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-enabled-true-and-send-on-resolved-true",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Enabled: &yes,
|
||||||
|
SendOnResolved: &yes,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "bf1436995a880eb4a352c74c5dfee1f1b5ff6b9fc55aef9bf411b3631adfd80c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-failure-threshold-7",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
FailureThreshold: 7,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "8bd479e18bda393d4c924f5a0d962e825002168dedaa88b445e435db7bacffd3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-failure-threshold-9",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
FailureThreshold: 9,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "5abdfce5236e344996d264d526e769c07cb0d3d329a999769a1ff84b157ca6f1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-success-threshold-5",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
SuccessThreshold: 7,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "c0000e73626b80e212cfc24830de7094568f648e37f3e16f9e68c7f8ef75c34c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-success-threshold-1",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
SuccessThreshold: 1,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "5c28963b3a76104cfa4a0d79c89dd29ec596c8cfa4b1af210ec83d6d41587b5f",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
scenario.alert.ValidateAndSetDefaults()
|
||||||
|
if checksum := scenario.alert.Checksum(); checksum != scenario.expected {
|
||||||
|
t.Errorf("expected checksum %v, got %v", scenario.expected, checksum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ const (
|
|||||||
// TypeGotify is the Type for the gotify alerting provider
|
// TypeGotify is the Type for the gotify alerting provider
|
||||||
TypeGotify Type = "gotify"
|
TypeGotify Type = "gotify"
|
||||||
|
|
||||||
|
// TypeJetBrainsSpace is the Type for the jetbrains alerting provider
|
||||||
|
TypeJetBrainsSpace Type = "jetbrainsspace"
|
||||||
|
|
||||||
// TypeMatrix is the Type for the matrix alerting provider
|
// TypeMatrix is the Type for the matrix alerting provider
|
||||||
TypeMatrix Type = "matrix"
|
TypeMatrix Type = "matrix"
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
|
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
||||||
@@ -54,6 +55,9 @@ type Config struct {
|
|||||||
// Gotify is the configuration for the gotify alerting provider
|
// Gotify is the configuration for the gotify alerting provider
|
||||||
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
|
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
|
||||||
|
|
||||||
|
// JetBrainsSpace is the configuration for the jetbrains space alerting provider
|
||||||
|
JetBrainsSpace *jetbrainsspace.AlertProvider `yaml:"jetbrainsspace,omitempty"`
|
||||||
|
|
||||||
// Matrix is the configuration for the matrix alerting provider
|
// Matrix is the configuration for the matrix alerting provider
|
||||||
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
|
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
|
||||||
|
|
||||||
@@ -102,7 +106,7 @@ func (config *Config) GetAlertingProviderByAlertType(alertType alert.Type) provi
|
|||||||
return fieldValue.Interface().(provider.AlertProvider)
|
return fieldValue.Interface().(provider.AlertProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[alerting][GetAlertingProviderByAlertType] No alerting provider found for alert type %s", alertType)
|
log.Printf("[alerting.GetAlertingProviderByAlertType] No alerting provider found for alert type %s", alertType)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package awsses
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/ses"
|
"github.com/aws/aws-sdk-go/service/ses"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,7 +50,6 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
registeredGroups[override.Group] = true
|
registeredGroups[override.Group] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate,
|
// 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.
|
// otherwise if neither are specified, then we'll fall back on IAM authentication.
|
||||||
return len(provider.From) > 0 && len(provider.To) > 0 &&
|
return len(provider.From) > 0 && len(provider.To) > 0 &&
|
||||||
@@ -57,14 +57,14 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
sess, err := provider.CreateSesSession()
|
sess, err := provider.createSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
svc := ses.New(sess)
|
svc := ses.New(sess)
|
||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||||
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
|
emails := strings.Split(provider.getToForGroup(ep.Group), ",")
|
||||||
|
|
||||||
input := &ses.SendEmailInput{
|
input := &ses.SendEmailInput{
|
||||||
Destination: &ses.Destination{
|
Destination: &ses.Destination{
|
||||||
@@ -110,29 +110,33 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildMessageSubjectAndBody builds the message subject and body
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message, results string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert triggered", ep.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)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
var formattedConditionResults string
|
||||||
var prefix string
|
if len(result.ConditionResults) > 0 {
|
||||||
if conditionResult.Success {
|
formattedConditionResults = "\n\nCondition results:\n"
|
||||||
prefix = "✅"
|
for _, conditionResult := range result.ConditionResults {
|
||||||
} else {
|
var prefix string
|
||||||
prefix = "❌"
|
if conditionResult.Success {
|
||||||
|
prefix = "✅"
|
||||||
|
} else {
|
||||||
|
prefix = "❌"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = "\n\nAlert description: " + alertDescription
|
description = "\n\nAlert description: " + alertDescription
|
||||||
}
|
}
|
||||||
return subject, message + description + "\n\nCondition results:\n" + results
|
return subject, message + description + formattedConditionResults
|
||||||
}
|
}
|
||||||
|
|
||||||
// getToForGroup returns the appropriate email integration to for a given group
|
// getToForGroup returns the appropriate email integration to for a given group
|
||||||
@@ -148,18 +152,16 @@ func (provider *AlertProvider) getToForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider AlertProvider) CreateSesSession() (*session.Session, error) {
|
func (provider *AlertProvider) createSession() (*session.Session, error) {
|
||||||
config := &aws.Config{
|
config := &aws.Config{
|
||||||
Region: aws.String(provider.Region),
|
Region: aws.String(provider.Region),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 {
|
if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 {
|
||||||
config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "")
|
config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.NewSession(config)
|
return session.NewSession(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
@@ -95,10 +95,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -116,10 +116,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
||||||
@@ -50,16 +50,16 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
func (provider *AlertProvider) buildHTTPRequest(ep *endpoint.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
||||||
body, url, method := provider.Body, provider.URL, provider.Method
|
body, url, method := provider.Body, provider.URL, provider.Method
|
||||||
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
|
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
|
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
|
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
|
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
|
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
|
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
|
||||||
if resolved {
|
if resolved {
|
||||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
@@ -78,8 +78,8 @@ func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
request := provider.buildHTTPRequest(endpoint, alert, resolved)
|
request := provider.buildHTTPRequest(ep, alert, resolved)
|
||||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -93,6 +93,6 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,10 +90,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -138,7 +138,7 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
request := customAlertProvider.buildHTTPRequest(
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
||||||
&alert.Alert{Description: &alertDescription},
|
&alert.Alert{Description: &alertDescription},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
@@ -188,7 +188,7 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
request := customAlertProvider.buildHTTPRequest(
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&alert.Alert{Description: &alertDescription},
|
&alert.Alert{Description: &alertDescription},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
@@ -217,10 +217,10 @@ func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Discord
|
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||||
@@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ type Embed struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Color int `json:"color"`
|
Color int `json:"color"`
|
||||||
Fields []Field `json:"fields"`
|
Fields []Field `json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
@@ -85,16 +85,17 @@ type Field struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, results string
|
var message string
|
||||||
var colorCode int
|
var colorCode int
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
colorCode = 3066993
|
colorCode = 3066993
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
colorCode = 15158332
|
colorCode = 15158332
|
||||||
}
|
}
|
||||||
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -102,7 +103,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
} else {
|
} else {
|
||||||
prefix = ":x:"
|
prefix = ":x:"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
@@ -112,24 +113,25 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
if provider.Title != "" {
|
if provider.Title != "" {
|
||||||
title = provider.Title
|
title = provider.Title
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body := Body{
|
||||||
Content: "",
|
Content: "",
|
||||||
Embeds: []Embed{
|
Embeds: []Embed{
|
||||||
{
|
{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: message + description,
|
Description: message + description,
|
||||||
Color: colorCode,
|
Color: colorCode,
|
||||||
Fields: []Field{
|
|
||||||
{
|
|
||||||
Name: "Condition results",
|
|
||||||
Value: results,
|
|
||||||
Inline: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
return body
|
if len(formattedConditionResults) > 0 {
|
||||||
|
body.Embeds[0].Fields = append(body.Embeds[0].Fields, Field{
|
||||||
|
Name: "Condition results",
|
||||||
|
Value: formattedConditionResults,
|
||||||
|
Inline: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||||
@@ -145,6 +147,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,10 +127,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -155,6 +155,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
Resolved bool
|
Resolved bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
@@ -179,18 +180,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Resolved: false,
|
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}]}]}",
|
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}]}]}",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
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}]}",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
||||||
|
}
|
||||||
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: conditionResults,
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
|
||||||
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
@@ -206,10 +219,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
gomail "gopkg.in/mail.v2"
|
gomail "gopkg.in/mail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,17 +53,17 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
var username string
|
var username string
|
||||||
if len(provider.Username) > 0 {
|
if len(provider.Username) > 0 {
|
||||||
username = provider.Username
|
username = provider.Username
|
||||||
} else {
|
} else {
|
||||||
username = provider.From
|
username = provider.From
|
||||||
}
|
}
|
||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", provider.From)
|
m.SetHeader("From", provider.From)
|
||||||
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
|
m.SetHeader("To", strings.Split(provider.getToForGroup(ep.Group), ",")...)
|
||||||
m.SetHeader("Subject", subject)
|
m.SetHeader("Subject", subject)
|
||||||
m.SetBody("text/plain", body)
|
m.SetBody("text/plain", body)
|
||||||
var d *gomail.Dialer
|
var d *gomail.Dialer
|
||||||
@@ -87,29 +87,33 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildMessageSubjectAndBody builds the message subject and body
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message, results string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert triggered", ep.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)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
var formattedConditionResults string
|
||||||
var prefix string
|
if len(result.ConditionResults) > 0 {
|
||||||
if conditionResult.Success {
|
formattedConditionResults = "\n\nCondition results:\n"
|
||||||
prefix = "✅"
|
for _, conditionResult := range result.ConditionResults {
|
||||||
} else {
|
var prefix string
|
||||||
prefix = "❌"
|
if conditionResult.Success {
|
||||||
|
prefix = "✅"
|
||||||
|
} else {
|
||||||
|
prefix = "❌"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = "\n\nAlert description: " + alertDescription
|
description = "\n\nAlert description: " + alertDescription
|
||||||
}
|
}
|
||||||
return subject, message + description + "\n\nCondition results:\n" + results
|
return subject, message + description + formattedConditionResults
|
||||||
}
|
}
|
||||||
|
|
||||||
// getToForGroup returns the appropriate email integration to for a given group
|
// getToForGroup returns the appropriate email integration to for a given group
|
||||||
@@ -125,6 +129,6 @@ func (provider *AlertProvider) getToForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
@@ -97,10 +97,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -118,10 +118,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/google/go-github/v48/github"
|
"github.com/google/go-github/v48/github"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
@@ -70,12 +70,12 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
title := "alert(gatus): " + endpoint.DisplayName()
|
title := "alert(gatus): " + ep.DisplayName()
|
||||||
if !resolved {
|
if !resolved {
|
||||||
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
||||||
Title: github.String(title),
|
Title: github.String(title),
|
||||||
Body: github.String(provider.buildIssueBody(endpoint, alert, result)),
|
Body: github.String(provider.buildIssueBody(ep, alert, result)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create issue: %w", err)
|
return fmt.Errorf("failed to create issue: %w", err)
|
||||||
@@ -104,26 +104,29 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildIssueBody builds the body of the issue
|
// buildIssueBody builds the body of the issue
|
||||||
func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string {
|
func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result) string {
|
||||||
var results string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
if len(result.ConditionResults) > 0 {
|
||||||
var prefix string
|
formattedConditionResults = "\n\n## Condition results\n"
|
||||||
if conditionResult.Success {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
prefix = ":white_check_mark:"
|
var prefix string
|
||||||
} else {
|
if conditionResult.Success {
|
||||||
prefix = ":x:"
|
prefix = ":white_check_mark:"
|
||||||
|
} else {
|
||||||
|
prefix = ":x:"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
|
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = ":\n> " + alertDescription
|
description = ":\n> " + alertDescription
|
||||||
}
|
}
|
||||||
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
return message + description + "\n\n## Condition results\n" + results
|
return message + description + formattedConditionResults
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
"github.com/google/go-github/v48/github"
|
"github.com/google/go-github/v48/github"
|
||||||
)
|
)
|
||||||
@@ -85,10 +85,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
scenario.Provider.githubClient = github.NewClient(nil)
|
scenario.Provider.githubClient = github.NewClient(nil)
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -109,37 +109,48 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no-description",
|
Name: "triggered-with-no-description",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{FailureThreshold: 10},
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
|
||||||
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
|
}
|
||||||
|
}
|
||||||
body := scenario.Provider.buildIssueBody(
|
body := scenario.Provider.buildIssueBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
ConditionResults: []*core.ConditionResult{
|
|
||||||
{Condition: "[CONNECTED] == true", Success: true},
|
|
||||||
{Condition: "[STATUS] == 200", Success: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
||||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
@@ -149,10 +160,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,10 +25,13 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// Severity can be one of: critical, high, medium, low, info, unknown. Defaults to critical
|
// Severity can be one of: critical, high, medium, low, info, unknown. Defaults to critical
|
||||||
Severity string `yaml:"severity,omitempty"`
|
Severity string `yaml:"severity,omitempty"`
|
||||||
|
|
||||||
// MonitoringTool overrides the name sent to gitlab. Defaults to gatus
|
// MonitoringTool overrides the name sent to gitlab. Defaults to gatus
|
||||||
MonitoringTool string `yaml:"monitoring-tool,omitempty"`
|
MonitoringTool string `yaml:"monitoring-tool,omitempty"`
|
||||||
|
|
||||||
// EnvironmentName is the name of the associated GitLab environment. Required to display alerts on a dashboard.
|
// EnvironmentName is the name of the associated GitLab environment. Required to display alerts on a dashboard.
|
||||||
EnvironmentName string `yaml:"environment-name,omitempty"`
|
EnvironmentName string `yaml:"environment-name,omitempty"`
|
||||||
|
|
||||||
// Service affected. Defaults to endpoint display name
|
// Service affected. Defaults to endpoint display name
|
||||||
Service string `yaml:"service,omitempty"`
|
Service string `yaml:"service,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -48,12 +51,11 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
if len(alert.ResolveKey) == 0 {
|
if len(alert.ResolveKey) == 0 {
|
||||||
alert.ResolveKey = uuid.NewString()
|
alert.ResolveKey = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
buffer := bytes.NewBuffer(provider.buildAlertBody(ep, alert, result, resolved))
|
||||||
buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved))
|
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -92,21 +94,21 @@ func (provider *AlertProvider) monitoringTool() string {
|
|||||||
return "gatus"
|
return "gatus"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) service(endpoint *core.Endpoint) string {
|
func (provider *AlertProvider) service(ep *endpoint.Endpoint) string {
|
||||||
if len(provider.Service) > 0 {
|
if len(provider.Service) > 0 {
|
||||||
return provider.Service
|
return provider.Service
|
||||||
}
|
}
|
||||||
return endpoint.DisplayName()
|
return ep.DisplayName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildAlertBody builds the body of the alert
|
// buildAlertBody builds the body of the alert
|
||||||
func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildAlertBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body := AlertBody{
|
body := AlertBody{
|
||||||
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(endpoint)),
|
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(ep)),
|
||||||
StartTime: result.Timestamp.Format(time.RFC3339),
|
StartTime: result.Timestamp.Format(time.RFC3339),
|
||||||
Service: provider.service(endpoint),
|
Service: provider.service(ep),
|
||||||
MonitoringTool: provider.monitoringTool(),
|
MonitoringTool: provider.monitoringTool(),
|
||||||
Hosts: endpoint.URL,
|
Hosts: ep.URL,
|
||||||
GitlabEnvironmentName: provider.EnvironmentName,
|
GitlabEnvironmentName: provider.EnvironmentName,
|
||||||
Severity: provider.Severity,
|
Severity: provider.Severity,
|
||||||
Fingerprint: alert.ResolveKey,
|
Fingerprint: alert.ResolveKey,
|
||||||
@@ -114,16 +116,18 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
|
|||||||
if resolved {
|
if resolved {
|
||||||
body.EndTime = result.Timestamp.Format(time.RFC3339)
|
body.EndTime = result.Timestamp.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
var formattedConditionResults string
|
||||||
var results string
|
if len(result.ConditionResults) > 0 {
|
||||||
for _, conditionResult := range result.ConditionResults {
|
formattedConditionResults = "\n\n## Condition results\n"
|
||||||
var prefix string
|
for _, conditionResult := range result.ConditionResults {
|
||||||
if conditionResult.Success {
|
var prefix string
|
||||||
prefix = ":white_check_mark:"
|
if conditionResult.Success {
|
||||||
} else {
|
prefix = ":white_check_mark:"
|
||||||
prefix = ":x:"
|
} else {
|
||||||
|
prefix = ":x:"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
|
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
@@ -131,17 +135,16 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
|
|||||||
}
|
}
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
body.Description = message + description + "\n\n## Condition results\n" + results
|
body.Description = message + description + formattedConditionResults
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
json, _ := json.Marshal(body)
|
return bodyAsJSON
|
||||||
return json
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,10 +84,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -108,21 +108,21 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
|||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no-description",
|
Name: "no-description",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{FailureThreshold: 10},
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||||
@@ -133,8 +133,8 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildAlertBody(
|
body := scenario.Provider.buildAlertBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: true},
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
{Condition: "[STATUS] == 200", Success: false},
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
},
|
},
|
||||||
@@ -149,10 +149,10 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
||||||
@@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ type OpenLink struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
@@ -121,7 +121,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
message = fmt.Sprintf("<font color='%s'>An alert has been triggered due to having failed %d time(s) in a row</font>", color, alert.FailureThreshold)
|
message = fmt.Sprintf("<font color='%s'>An alert has been triggered due to having failed %d time(s) in a row</font>", color, alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var results string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -129,7 +129,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
} else {
|
} else {
|
||||||
prefix = "❌"
|
prefix = "❌"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s %s<br>", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s %s<br>", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
@@ -143,28 +143,30 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
Widgets: []Widgets{
|
Widgets: []Widgets{
|
||||||
{
|
{
|
||||||
KeyValue: &KeyValue{
|
KeyValue: &KeyValue{
|
||||||
TopLabel: endpoint.DisplayName(),
|
TopLabel: ep.DisplayName(),
|
||||||
Content: message,
|
Content: message,
|
||||||
ContentMultiline: "true",
|
ContentMultiline: "true",
|
||||||
BottomLabel: description,
|
BottomLabel: description,
|
||||||
Icon: "BOOKMARK",
|
Icon: "BOOKMARK",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
KeyValue: &KeyValue{
|
|
||||||
TopLabel: "Condition results",
|
|
||||||
Content: results,
|
|
||||||
ContentMultiline: "true",
|
|
||||||
Icon: "DESCRIPTION",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if endpoint.Type() == core.EndpointTypeHTTP {
|
if len(formattedConditionResults) > 0 {
|
||||||
|
payload.Cards[0].Sections[0].Widgets = append(payload.Cards[0].Sections[0].Widgets, Widgets{
|
||||||
|
KeyValue: &KeyValue{
|
||||||
|
TopLabel: "Condition results",
|
||||||
|
Content: formattedConditionResults,
|
||||||
|
ContentMultiline: "true",
|
||||||
|
Icon: "DESCRIPTION",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if ep.Type() == endpoint.TypeHTTP {
|
||||||
// We only include a button targeting the URL if the endpoint is an HTTP endpoint
|
// We only include a button targeting the URL if the endpoint is an HTTP endpoint
|
||||||
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
|
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
|
||||||
// See https://github.com/TwiN/gatus/issues/362
|
// See https://github.com/TwiN/gatus/issues/362
|
||||||
@@ -173,14 +175,14 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
{
|
{
|
||||||
TextButton: TextButton{
|
TextButton: TextButton{
|
||||||
Text: "URL",
|
Text: "URL",
|
||||||
OnClick: OnClick{OpenLink: OpenLink{URL: endpoint.URL}},
|
OnClick: OnClick{OpenLink: OpenLink{URL: ep.URL}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
bodyAsJSON, _ := json.Marshal(payload)
|
||||||
return body
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||||
@@ -196,6 +198,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -141,7 +141,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
secondDescription := "description-2"
|
secondDescription := "description-2"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
Resolved bool
|
Resolved bool
|
||||||
@@ -149,7 +149,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
@@ -157,7 +157,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
@@ -165,7 +165,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
@@ -173,7 +173,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
@@ -185,8 +185,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -205,10 +205,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultPriority = 5
|
const DefaultPriority = 5
|
||||||
@@ -41,8 +41,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -67,13 +67,14 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, results string
|
var message string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -81,25 +82,25 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
} else {
|
} else {
|
||||||
prefix = "✕"
|
prefix = "✕"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
if len(alert.GetDescription()) > 0 {
|
if len(alert.GetDescription()) > 0 {
|
||||||
message += " with the following description: " + alert.GetDescription()
|
message += " with the following description: " + alert.GetDescription()
|
||||||
}
|
}
|
||||||
message += results
|
message += formattedConditionResults
|
||||||
title := "Gatus: " + endpoint.DisplayName()
|
title := "Gatus: " + ep.DisplayName()
|
||||||
if provider.Title != "" {
|
if provider.Title != "" {
|
||||||
title = provider.Title
|
title = provider.Title
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
bodyAsJSON, _ := json.Marshal(Body{
|
||||||
Message: message,
|
Message: message,
|
||||||
Title: title,
|
Title: title,
|
||||||
Priority: provider.Priority,
|
Priority: provider.Priority,
|
||||||
})
|
})
|
||||||
return body
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
@@ -49,7 +49,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
var (
|
var (
|
||||||
description = "custom-description"
|
description = "custom-description"
|
||||||
//title = "custom-title"
|
//title = "custom-title"
|
||||||
endpoint = "custom-endpoint"
|
endpointName = "custom-endpoint"
|
||||||
)
|
)
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -63,30 +63,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
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),
|
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}", endpointName, description),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
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),
|
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}", endpointName, description),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "custom-title",
|
Name: "custom-title",
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
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),
|
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}", endpointName, description),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: endpoint},
|
&endpoint.Endpoint{Name: endpointName},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package jetbrainsspace
|
||||||
|
|
||||||
|
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/config/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
|
||||||
|
type AlertProvider struct {
|
||||||
|
Project string `yaml:"project"` // JetBrains Space Project name
|
||||||
|
ChannelID string `yaml:"channel-id"` // JetBrains Space Chat Channel ID
|
||||||
|
Token string `yaml:"token"` // JetBrains Space Bearer Token
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
ChannelID string `yaml:"channel-id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.ChannelID) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
registeredGroups[override.Group] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(provider.Project) > 0 && len(provider.ChannelID) > 0 && len(provider.Token) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an alert using the provider
|
||||||
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
|
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
|
||||||
|
request, err := http.NewRequest(http.MethodPost, url, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
request.Header.Set("Authorization", "Bearer "+provider.Token)
|
||||||
|
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("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Body struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Content Content `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Content struct {
|
||||||
|
ClassName string `json:"className"`
|
||||||
|
Style string `json:"style"`
|
||||||
|
Sections []Section `json:"sections,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
ClassName string `json:"className"`
|
||||||
|
Elements []Element `json:"elements"`
|
||||||
|
Header string `json:"header"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Element struct {
|
||||||
|
ClassName string `json:"className"`
|
||||||
|
Accessory Accessory `json:"accessory"`
|
||||||
|
Style string `json:"style"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Accessory struct {
|
||||||
|
ClassName string `json:"className"`
|
||||||
|
Icon Icon `json:"icon"`
|
||||||
|
Style string `json:"style"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Icon struct {
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRequestBody builds the request body for the provider
|
||||||
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
|
body := Body{
|
||||||
|
Channel: "id:" + provider.getChannelIDForGroup(ep.Group),
|
||||||
|
Content: Content{
|
||||||
|
ClassName: "ChatMessage.Block",
|
||||||
|
Sections: []Section{{
|
||||||
|
ClassName: "MessageSection",
|
||||||
|
Elements: []Element{},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if resolved {
|
||||||
|
body.Content.Style = "SUCCESS"
|
||||||
|
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
body.Content.Style = "WARNING"
|
||||||
|
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
icon := "warning"
|
||||||
|
style := "WARNING"
|
||||||
|
if conditionResult.Success {
|
||||||
|
icon = "success"
|
||||||
|
style = "SUCCESS"
|
||||||
|
}
|
||||||
|
body.Content.Sections[0].Elements = append(body.Content.Sections[0].Elements, Element{
|
||||||
|
ClassName: "MessageText",
|
||||||
|
Accessory: Accessory{
|
||||||
|
ClassName: "MessageIcon",
|
||||||
|
Icon: Icon{Icon: icon},
|
||||||
|
Style: style,
|
||||||
|
},
|
||||||
|
Style: style,
|
||||||
|
Size: "REGULAR",
|
||||||
|
Content: conditionResult.Condition,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
return bodyAsJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// getChannelIDForGroup returns the appropriate channel ID to for a given group override
|
||||||
|
func (provider *AlertProvider) getChannelIDForGroup(group string) string {
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if group == override.Group {
|
||||||
|
return override.ChannelID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.ChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package jetbrainsspace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{Project: ""}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
validProvider := AlertProvider{Project: "foo", ChannelID: "bar", Token: "baz"}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||||
|
providerWithInvalidOverrideGroup := AlertProvider{
|
||||||
|
Project: "foobar",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
ChannelID: "http://example.com",
|
||||||
|
Group: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideGroup.IsValid() {
|
||||||
|
t.Error("provider Group shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithInvalidOverrideTo := AlertProvider{
|
||||||
|
Project: "foobar",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
ChannelID: "",
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideTo.IsValid() {
|
||||||
|
t.Error("provider integration key shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithValidOverride := AlertProvider{
|
||||||
|
Project: "foo",
|
||||||
|
ChannelID: "bar",
|
||||||
|
Token: "baz",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
ChannelID: "foobar",
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !providerWithValidOverride.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
|
defer client.InjectHTTPClient(nil)
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
MockRoundTripper test.MockRoundTripper
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-error",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
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.StatusInternalServerError, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-error",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
|
err := scenario.Provider.Send(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if scenario.ExpectedError && err == nil {
|
||||||
|
t.Error("expected error, got none")
|
||||||
|
}
|
||||||
|
if !scenario.ExpectedError && err != nil {
|
||||||
|
t.Error("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Endpoint endpoint.Endpoint
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
ExpectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-group",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-with-group",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
body := scenario.Provider.buildRequestBody(
|
||||||
|
&scenario.Endpoint,
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_getChannelIDForGroup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
InputGroup string
|
||||||
|
ExpectedOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
ChannelID: "bar",
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
ChannelID: "bar",
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
ChannelID: "bar",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
ChannelID: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-group-should-override",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
ChannelID: "bar",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
ChannelID: "foobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: "foobar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
if got := tt.Provider.getChannelIDForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||||
|
t.Errorf("AlertProvider.getChannelIDForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Matrix
|
// AlertProvider is the configuration necessary for sending an alert using Matrix
|
||||||
type AlertProvider struct {
|
type AlertProvider struct {
|
||||||
MatrixProviderConfig `yaml:",inline"`
|
ProviderConfig `yaml:",inline"`
|
||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
@@ -30,12 +30,12 @@ type AlertProvider struct {
|
|||||||
type Override struct {
|
type Override struct {
|
||||||
Group string `yaml:"group"`
|
Group string `yaml:"group"`
|
||||||
|
|
||||||
MatrixProviderConfig `yaml:",inline"`
|
ProviderConfig `yaml:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHomeserverURL = "https://matrix-client.matrix.org"
|
const defaultServerURL = "https://matrix-client.matrix.org"
|
||||||
|
|
||||||
type MatrixProviderConfig struct {
|
type ProviderConfig struct {
|
||||||
// ServerURL is the custom homeserver to use (optional)
|
// ServerURL is the custom homeserver to use (optional)
|
||||||
ServerURL string `yaml:"server-url"`
|
ServerURL string `yaml:"server-url"`
|
||||||
|
|
||||||
@@ -61,11 +61,11 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
config := provider.getConfigForGroup(endpoint.Group)
|
config := provider.getConfigForGroup(ep.Group)
|
||||||
if config.ServerURL == "" {
|
if config.ServerURL == "" {
|
||||||
config.ServerURL = defaultHomeserverURL
|
config.ServerURL = defaultServerURL
|
||||||
}
|
}
|
||||||
// The Matrix endpoint requires a unique transaction ID for each event sent
|
// The Matrix endpoint requires a unique transaction ID for each event sent
|
||||||
txnId := randStringBytes(24)
|
txnId := randStringBytes(24)
|
||||||
@@ -103,24 +103,25 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
MsgType: "m.text",
|
MsgType: "m.text",
|
||||||
Format: "org.matrix.custom.html",
|
Format: "org.matrix.custom.html",
|
||||||
Body: buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
Body: buildPlaintextMessageBody(ep, alert, result, resolved),
|
||||||
FormattedBody: buildHTMLMessageBody(endpoint, alert, result, resolved),
|
FormattedBody: buildHTMLMessageBody(ep, alert, result, resolved),
|
||||||
})
|
})
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
||||||
func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func buildPlaintextMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message, results string
|
var message string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -128,49 +129,54 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu
|
|||||||
} else {
|
} else {
|
||||||
prefix = "✕"
|
prefix = "✕"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = "\n" + alertDescription
|
description = "\n" + alertDescription
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s%s\n%s", message, description, results)
|
return fmt.Sprintf("%s%s\n%s", message, description, formattedConditionResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildHTMLMessageBody builds the message body in HTML to include in request
|
// buildHTMLMessageBody builds the message body in HTML to include in request
|
||||||
func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func buildHTMLMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message, results string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
var formattedConditionResults string
|
||||||
var prefix string
|
if len(result.ConditionResults) > 0 {
|
||||||
if conditionResult.Success {
|
formattedConditionResults = "\n<h5>Condition results</h5><ul>"
|
||||||
prefix = "✅"
|
for _, conditionResult := range result.ConditionResults {
|
||||||
} else {
|
var prefix string
|
||||||
prefix = "❌"
|
if conditionResult.Success {
|
||||||
|
prefix = "✅"
|
||||||
|
} else {
|
||||||
|
prefix = "❌"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("<li>%s - <code>%s</code></li>", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("<li>%s - <code>%s</code></li>", prefix, conditionResult.Condition)
|
formattedConditionResults += "</ul>"
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = fmt.Sprintf("\n<blockquote>%s</blockquote>", alertDescription)
|
description = fmt.Sprintf("\n<blockquote>%s</blockquote>", alertDescription)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("<h3>%s</h3>%s\n<h5>Condition results</h5><ul>%s</ul>", message, description, results)
|
return fmt.Sprintf("<h3>%s</h3>%s%s", message, description, formattedConditionResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConfigForGroup returns the appropriate configuration for a given group
|
// getConfigForGroup returns the appropriate configuration for a given group
|
||||||
func (provider *AlertProvider) getConfigForGroup(group string) MatrixProviderConfig {
|
func (provider *AlertProvider) getConfigForGroup(group string) ProviderConfig {
|
||||||
if provider.Overrides != nil {
|
if provider.Overrides != nil {
|
||||||
for _, override := range provider.Overrides {
|
for _, override := range provider.Overrides {
|
||||||
if group == override.Group {
|
if group == override.Group {
|
||||||
return override.MatrixProviderConfig
|
return override.ProviderConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return provider.MatrixProviderConfig
|
return provider.ProviderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func randStringBytes(n int) string {
|
func randStringBytes(n int) string {
|
||||||
@@ -185,6 +191,6 @@ func randStringBytes(n int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{
|
invalidProvider := AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
AccessToken: "",
|
AccessToken: "",
|
||||||
InternalRoomID: "",
|
InternalRoomID: "",
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||||||
t.Error("provider shouldn't have been valid")
|
t.Error("provider shouldn't have been valid")
|
||||||
}
|
}
|
||||||
validProvider := AlertProvider{
|
validProvider := AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||||||
t.Error("provider should've been valid")
|
t.Error("provider should've been valid")
|
||||||
}
|
}
|
||||||
validProviderWithHomeserver := AlertProvider{
|
validProviderWithHomeserver := AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -47,7 +47,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
|||||||
Overrides: []Override{
|
Overrides: []Override{
|
||||||
{
|
{
|
||||||
Group: "",
|
Group: "",
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
AccessToken: "",
|
AccessToken: "",
|
||||||
InternalRoomID: "",
|
InternalRoomID: "",
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
|||||||
Overrides: []Override{
|
Overrides: []Override{
|
||||||
{
|
{
|
||||||
Group: "group",
|
Group: "group",
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
AccessToken: "",
|
AccessToken: "",
|
||||||
InternalRoomID: "",
|
InternalRoomID: "",
|
||||||
},
|
},
|
||||||
@@ -72,14 +72,14 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
|||||||
t.Error("provider integration key shouldn't have been valid")
|
t.Error("provider integration key shouldn't have been valid")
|
||||||
}
|
}
|
||||||
providerWithValidOverride := AlertProvider{
|
providerWithValidOverride := AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
},
|
},
|
||||||
Overrides: []Override{
|
Overrides: []Override{
|
||||||
{
|
{
|
||||||
Group: "group",
|
Group: "group",
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -149,10 +149,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -197,10 +197,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -219,10 +219,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,12 +232,12 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
InputGroup string
|
InputGroup string
|
||||||
ExpectedOutput MatrixProviderConfig
|
ExpectedOutput ProviderConfig
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "provider-no-override-specify-no-group-should-default",
|
Name: "provider-no-override-specify-no-group-should-default",
|
||||||
Provider: AlertProvider{
|
Provider: AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -245,7 +245,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
Overrides: nil,
|
Overrides: nil,
|
||||||
},
|
},
|
||||||
InputGroup: "",
|
InputGroup: "",
|
||||||
ExpectedOutput: MatrixProviderConfig{
|
ExpectedOutput: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -254,7 +254,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "provider-no-override-specify-group-should-default",
|
Name: "provider-no-override-specify-group-should-default",
|
||||||
Provider: AlertProvider{
|
Provider: AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -262,7 +262,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
Overrides: nil,
|
Overrides: nil,
|
||||||
},
|
},
|
||||||
InputGroup: "group",
|
InputGroup: "group",
|
||||||
ExpectedOutput: MatrixProviderConfig{
|
ExpectedOutput: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -271,7 +271,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "provider-with-override-specify-no-group-should-default",
|
Name: "provider-with-override-specify-no-group-should-default",
|
||||||
Provider: AlertProvider{
|
Provider: AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -279,7 +279,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
Overrides: []Override{
|
Overrides: []Override{
|
||||||
{
|
{
|
||||||
Group: "group",
|
Group: "group",
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example01.com",
|
ServerURL: "https://example01.com",
|
||||||
AccessToken: "12",
|
AccessToken: "12",
|
||||||
InternalRoomID: "!a:example01.com",
|
InternalRoomID: "!a:example01.com",
|
||||||
@@ -288,7 +288,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
InputGroup: "",
|
InputGroup: "",
|
||||||
ExpectedOutput: MatrixProviderConfig{
|
ExpectedOutput: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -297,7 +297,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "provider-with-override-specify-group-should-override",
|
Name: "provider-with-override-specify-group-should-override",
|
||||||
Provider: AlertProvider{
|
Provider: AlertProvider{
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example.com",
|
ServerURL: "https://example.com",
|
||||||
AccessToken: "1",
|
AccessToken: "1",
|
||||||
InternalRoomID: "!a:example.com",
|
InternalRoomID: "!a:example.com",
|
||||||
@@ -305,7 +305,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
Overrides: []Override{
|
Overrides: []Override{
|
||||||
{
|
{
|
||||||
Group: "group",
|
Group: "group",
|
||||||
MatrixProviderConfig: MatrixProviderConfig{
|
ProviderConfig: ProviderConfig{
|
||||||
ServerURL: "https://example01.com",
|
ServerURL: "https://example01.com",
|
||||||
AccessToken: "12",
|
AccessToken: "12",
|
||||||
InternalRoomID: "!a:example01.com",
|
InternalRoomID: "!a:example01.com",
|
||||||
@@ -314,7 +314,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
InputGroup: "group",
|
InputGroup: "group",
|
||||||
ExpectedOutput: MatrixProviderConfig{
|
ExpectedOutput: ProviderConfig{
|
||||||
ServerURL: "https://example01.com",
|
ServerURL: "https://example01.com",
|
||||||
AccessToken: "12",
|
AccessToken: "12",
|
||||||
InternalRoomID: "!a:example01.com",
|
InternalRoomID: "!a:example01.com",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
||||||
@@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -92,29 +92,32 @@ type Field struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color, results string
|
var message, color string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
var formattedConditionResults string
|
||||||
var prefix string
|
if len(result.ConditionResults) > 0 {
|
||||||
if conditionResult.Success {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
prefix = ":white_check_mark:"
|
var prefix string
|
||||||
} else {
|
if conditionResult.Success {
|
||||||
prefix = ":x:"
|
prefix = ":white_check_mark:"
|
||||||
|
} else {
|
||||||
|
prefix = ":x:"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = ":\n> " + alertDescription
|
description = ":\n> " + alertDescription
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body := Body{
|
||||||
Text: "",
|
Text: "",
|
||||||
Username: "gatus",
|
Username: "gatus",
|
||||||
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
||||||
@@ -125,17 +128,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
Text: message + description,
|
Text: message + description,
|
||||||
Short: false,
|
Short: false,
|
||||||
Color: color,
|
Color: color,
|
||||||
Fields: []Field{
|
|
||||||
{
|
|
||||||
Title: "Condition results",
|
|
||||||
Value: results,
|
|
||||||
Short: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
return body
|
if len(formattedConditionResults) > 0 {
|
||||||
|
body.Attachments[0].Fields = append(body.Attachments[0].Fields, Field{
|
||||||
|
Title: "Condition results",
|
||||||
|
Value: formattedConditionResults,
|
||||||
|
Short: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||||
@@ -151,6 +155,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
if providerWithInvalidOverrideWebHookUrl.IsValid() {
|
if providerWithInvalidOverrideWebHookUrl.IsValid() {
|
||||||
t.Error("provider WebHookURL shoudn't have been valid")
|
t.Error("provider WebHookURL shouldn't have been valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
providerWithValidOverride := AlertProvider{
|
providerWithValidOverride := AlertProvider{
|
||||||
@@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -168,10 +168,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -190,10 +190,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,8 +33,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -60,12 +60,12 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Originator: provider.Originator,
|
Originator: provider.Originator,
|
||||||
@@ -76,6 +76,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,10 +83,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -131,10 +131,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -153,10 +153,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -77,8 +77,8 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, results, tag string
|
var message, formattedConditionResults, tag string
|
||||||
if resolved {
|
if resolved {
|
||||||
tag = "white_check_mark"
|
tag = "white_check_mark"
|
||||||
message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row"
|
message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row"
|
||||||
@@ -93,15 +93,15 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
} else {
|
} else {
|
||||||
prefix = "🔴"
|
prefix = "🔴"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
if len(alert.GetDescription()) > 0 {
|
if len(alert.GetDescription()) > 0 {
|
||||||
message += " with the following description: " + alert.GetDescription()
|
message += " with the following description: " + alert.GetDescription()
|
||||||
}
|
}
|
||||||
message += results
|
message += formattedConditionResults
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Topic: provider.Topic,
|
Topic: provider.Topic,
|
||||||
Title: "Gatus: " + endpoint.DisplayName(),
|
Title: "Gatus: " + ep.DisplayName(),
|
||||||
Message: message,
|
Message: message,
|
||||||
Tags: []string{tag},
|
Tags: []string{tag},
|
||||||
Priority: provider.Priority,
|
Priority: provider.Priority,
|
||||||
@@ -110,6 +110,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
@@ -92,10 +92,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -59,13 +59,13 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
//
|
//
|
||||||
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
err := provider.createAlert(endpoint, alert, result, resolved)
|
err := provider.createAlert(ep, alert, result, resolved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resolved {
|
if resolved {
|
||||||
err = provider.closeAlert(endpoint, alert)
|
err = provider.closeAlert(ep, alert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -75,20 +75,20 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
||||||
alert.ResolveKey = ""
|
alert.ResolveKey = ""
|
||||||
} else {
|
} else {
|
||||||
alert.ResolveKey = provider.alias(buildKey(endpoint))
|
alert.ResolveKey = provider.alias(buildKey(ep))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) createAlert(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) createAlert(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
payload := provider.buildCreateRequestBody(endpoint, alert, result, resolved)
|
payload := provider.buildCreateRequestBody(ep, alert, result, resolved)
|
||||||
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) closeAlert(endpoint *core.Endpoint, alert *alert.Alert) error {
|
func (provider *AlertProvider) closeAlert(ep *endpoint.Endpoint, alert *alert.Alert) error {
|
||||||
payload := provider.buildCloseRequestBody(endpoint, alert)
|
payload := provider.buildCloseRequestBody(ep, alert)
|
||||||
url := restAPI + "/" + provider.alias(buildKey(endpoint)) + "/close?identifierType=alias"
|
url := restAPI + "/" + provider.alias(buildKey(ep)) + "/close?identifierType=alias"
|
||||||
return provider.sendRequest(url, http.MethodPost, payload)
|
return provider.sendRequest(url, http.MethodPost, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,18 +115,19 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) alertCreateRequest {
|
func (provider *AlertProvider) buildCreateRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) alertCreateRequest {
|
||||||
var message, description, results string
|
var message, description string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription())
|
||||||
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("%s - %s", endpoint.Name, alert.GetDescription())
|
message = fmt.Sprintf("%s - %s", ep.Name, alert.GetDescription())
|
||||||
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
if endpoint.Group != "" {
|
if ep.Group != "" {
|
||||||
message = fmt.Sprintf("[%s] %s", endpoint.Group, message)
|
message = fmt.Sprintf("[%s] %s", ep.Group, message)
|
||||||
}
|
}
|
||||||
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -134,13 +135,13 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
|
|||||||
} else {
|
} else {
|
||||||
prefix = "▢"
|
prefix = "▢"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
description = description + "\n" + results
|
description = description + "\n" + formattedConditionResults
|
||||||
key := buildKey(endpoint)
|
key := buildKey(ep)
|
||||||
details := map[string]string{
|
details := map[string]string{
|
||||||
"endpoint:url": endpoint.URL,
|
"endpoint:url": ep.URL,
|
||||||
"endpoint:group": endpoint.Group,
|
"endpoint:group": ep.Group,
|
||||||
"result:hostname": result.Hostname,
|
"result:hostname": result.Hostname,
|
||||||
"result:ip": result.IP,
|
"result:ip": result.IP,
|
||||||
"result:dns_code": result.DNSRCode,
|
"result:dns_code": result.DNSRCode,
|
||||||
@@ -166,10 +167,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildCloseRequestBody(endpoint *core.Endpoint, alert *alert.Alert) alertCloseRequest {
|
func (provider *AlertProvider) buildCloseRequestBody(ep *endpoint.Endpoint, alert *alert.Alert) alertCloseRequest {
|
||||||
return alertCloseRequest{
|
return alertCloseRequest{
|
||||||
Source: buildKey(endpoint),
|
Source: buildKey(ep),
|
||||||
Note: fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()),
|
Note: fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,16 +207,16 @@ func (provider *AlertProvider) priority() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKey(endpoint *core.Endpoint) string {
|
func buildKey(ep *endpoint.Endpoint) string {
|
||||||
name := toKebabCase(endpoint.Name)
|
name := toKebabCase(ep.Name)
|
||||||
if endpoint.Group == "" {
|
if ep.Group == "" {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
return toKebabCase(endpoint.Group) + "-" + name
|
return toKebabCase(ep.Group) + "-" + name
|
||||||
}
|
}
|
||||||
|
|
||||||
func toKebabCase(val string) string {
|
func toKebabCase(val string) string {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,10 +79,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -106,8 +106,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider *AlertProvider
|
Provider *AlertProvider
|
||||||
Alert *alert.Alert
|
Alert *alert.Alert
|
||||||
Endpoint *core.Endpoint
|
Endpoint *endpoint.Endpoint
|
||||||
Result *core.Result
|
Result *endpoint.Result
|
||||||
Resolved bool
|
Resolved bool
|
||||||
want alertCreateRequest
|
want alertCreateRequest
|
||||||
}{
|
}{
|
||||||
@@ -115,8 +115,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Name: "missing all params (unresolved)",
|
Name: "missing all params (unresolved)",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
Result: &core.Result{},
|
Result: &endpoint.Result{},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
want: alertCreateRequest{
|
want: alertCreateRequest{
|
||||||
Message: " - ",
|
Message: " - ",
|
||||||
@@ -133,8 +133,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Name: "missing all params (resolved)",
|
Name: "missing all params (resolved)",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
Result: &core.Result{},
|
Result: &endpoint.Result{},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
want: alertCreateRequest{
|
want: alertCreateRequest{
|
||||||
Message: "RESOLVED: - ",
|
Message: "RESOLVED: - ",
|
||||||
@@ -154,11 +154,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Description: &description,
|
Description: &description,
|
||||||
FailureThreshold: 3,
|
FailureThreshold: 3,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my super app",
|
Name: "my super app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -194,11 +194,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Description: &description,
|
Description: &description,
|
||||||
SuccessThreshold: 4,
|
SuccessThreshold: 4,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my mega app",
|
Name: "my mega app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -226,17 +226,17 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Description: &description,
|
Description: &description,
|
||||||
FailureThreshold: 6,
|
FailureThreshold: 6,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my app",
|
Name: "my app",
|
||||||
Group: "end game",
|
Group: "end game",
|
||||||
URL: "https://my.go/app",
|
URL: "https://my.go/app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
HTTPStatus: 400,
|
HTTPStatus: 400,
|
||||||
Hostname: "my.go",
|
Hostname: "my.go",
|
||||||
Errors: []string{"error 01", "error 02"},
|
Errors: []string{"error 01", "error 02"},
|
||||||
Success: false,
|
Success: false,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -279,14 +279,14 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider *AlertProvider
|
Provider *AlertProvider
|
||||||
Alert *alert.Alert
|
Alert *alert.Alert
|
||||||
Endpoint *core.Endpoint
|
Endpoint *endpoint.Endpoint
|
||||||
want alertCloseRequest
|
want alertCloseRequest
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "Missing all values",
|
Name: "Missing all values",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
want: alertCloseRequest{
|
want: alertCloseRequest{
|
||||||
Source: "",
|
Source: "",
|
||||||
Note: "RESOLVED: - ",
|
Note: "RESOLVED: - ",
|
||||||
@@ -298,7 +298,7 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
|||||||
Alert: &alert.Alert{
|
Alert: &alert.Alert{
|
||||||
Description: &description,
|
Description: &description,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "endpoint name",
|
Name: "endpoint name",
|
||||||
},
|
},
|
||||||
want: alertCloseRequest{
|
want: alertCloseRequest{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
//
|
//
|
||||||
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -78,7 +78,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
var payload pagerDutyResponsePayload
|
var payload pagerDutyResponsePayload
|
||||||
if err = json.Unmarshal(body, &payload); err != nil {
|
if err = json.Unmarshal(body, &payload); err != nil {
|
||||||
// Silently fail. We don't want to create tons of alerts just because we failed to parse the body.
|
// Silently fail. We don't want to create tons of alerts just because we failed to parse the body.
|
||||||
log.Printf("[pagerduty][Send] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
log.Printf("[pagerduty.Send] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
alert.ResolveKey = payload.DedupKey
|
alert.ResolveKey = payload.DedupKey
|
||||||
}
|
}
|
||||||
@@ -101,19 +101,19 @@ type Payload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, eventAction, resolveKey string
|
var message, eventAction, resolveKey string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
eventAction = "resolve"
|
eventAction = "resolve"
|
||||||
resolveKey = alert.ResolveKey
|
resolveKey = alert.ResolveKey
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
eventAction = "trigger"
|
eventAction = "trigger"
|
||||||
resolveKey = ""
|
resolveKey = ""
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
RoutingKey: provider.getIntegrationKeyForGroup(endpoint.Group),
|
RoutingKey: provider.getIntegrationKeyForGroup(ep.Group),
|
||||||
DedupKey: resolveKey,
|
DedupKey: resolveKey,
|
||||||
EventAction: eventAction,
|
EventAction: eventAction,
|
||||||
Payload: Payload{
|
Payload: Payload{
|
||||||
@@ -138,7 +138,7 @@ func (provider *AlertProvider) getIntegrationKeyForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -115,10 +115,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(&core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &core.Result{}, scenario.Resolved)
|
body := scenario.Provider.buildRequestBody(&endpoint.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &endpoint.Result{}, scenario.Resolved)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
}
|
}
|
||||||
@@ -237,10 +237,10 @@ func TestAlertProvider_getIntegrationKeyForGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
||||||
@@ -20,10 +21,10 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the interface that each providers should implement
|
// AlertProvider is the interface that each provider should implement
|
||||||
type AlertProvider interface {
|
type AlertProvider interface {
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
IsValid() bool
|
IsValid() bool
|
||||||
@@ -32,7 +33,7 @@ type AlertProvider interface {
|
|||||||
GetDefaultAlert() *alert.Alert
|
GetDefaultAlert() *alert.Alert
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error
|
Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
||||||
@@ -66,6 +67,7 @@ var (
|
|||||||
_ AlertProvider = (*github.AlertProvider)(nil)
|
_ AlertProvider = (*github.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*gitlab.AlertProvider)(nil)
|
_ AlertProvider = (*gitlab.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*jetbrainsspace.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// Reference doc for pushover: https://pushover.net/api
|
// Reference doc for pushover: https://pushover.net/api
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -81,12 +81,12 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Token: provider.ApplicationToken,
|
Token: provider.ApplicationToken,
|
||||||
@@ -107,6 +107,6 @@ func (provider *AlertProvider) priority() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,10 +95,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -150,10 +150,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -172,10 +172,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Slack
|
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||||
@@ -42,9 +42,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ type Attachment struct {
|
|||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Short bool `json:"short"`
|
Short bool `json:"short"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
Fields []Field `json:"fields"`
|
Fields []Field `json:"fields,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
@@ -81,15 +81,16 @@ type Field struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color, results string
|
var message, color string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -97,13 +98,13 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
} else {
|
} else {
|
||||||
prefix = ":x:"
|
prefix = ":x:"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = ":\n> " + alertDescription
|
description = ":\n> " + alertDescription
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body := Body{
|
||||||
Text: "",
|
Text: "",
|
||||||
Attachments: []Attachment{
|
Attachments: []Attachment{
|
||||||
{
|
{
|
||||||
@@ -111,17 +112,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
Text: message + description,
|
Text: message + description,
|
||||||
Short: false,
|
Short: false,
|
||||||
Color: color,
|
Color: color,
|
||||||
Fields: []Field{
|
|
||||||
{
|
|
||||||
Title: "Condition results",
|
|
||||||
Value: results,
|
|
||||||
Short: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
return body
|
if len(formattedConditionResults) > 0 {
|
||||||
|
body.Attachments[0].Fields = append(body.Attachments[0].Fields, Field{
|
||||||
|
Title: "Condition results",
|
||||||
|
Value: formattedConditionResults,
|
||||||
|
Short: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||||
@@ -137,6 +139,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -142,15 +142,16 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
Resolved bool
|
Resolved bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@@ -158,15 +159,24 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "triggered-with-group",
|
Name: "triggered-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@@ -174,7 +184,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "resolved-with-group",
|
Name: "resolved-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@@ -182,14 +192,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
}
|
||||||
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: conditionResults,
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
@@ -205,10 +219,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Teams
|
// AlertProvider is the configuration necessary for sending an alert using Teams
|
||||||
@@ -21,6 +21,9 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||||
Overrides []Override `yaml:"overrides,omitempty"`
|
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
|
// Override is a case under which the default integration is overridden
|
||||||
@@ -44,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,7 @@ type Body struct {
|
|||||||
ThemeColor string `json:"themeColor"`
|
ThemeColor string `json:"themeColor"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Sections []Section `json:"sections"`
|
Sections []Section `json:"sections,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Section struct {
|
type Section struct {
|
||||||
@@ -78,16 +81,16 @@ type Section struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
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)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var results string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
var prefix string
|
var prefix string
|
||||||
if conditionResult.Success {
|
if conditionResult.Success {
|
||||||
@@ -95,26 +98,30 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
} else {
|
} else {
|
||||||
prefix = "❌"
|
prefix = "❌"
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s - `%s`<br/>", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s - `%s`<br/>", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
var description string
|
var description string
|
||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = ": " + alertDescription
|
description = ": " + alertDescription
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body := Body{
|
||||||
Type: "MessageCard",
|
Type: "MessageCard",
|
||||||
Context: "http://schema.org/extensions",
|
Context: "http://schema.org/extensions",
|
||||||
ThemeColor: color,
|
ThemeColor: color,
|
||||||
Title: "🚨 Gatus",
|
Title: provider.Title,
|
||||||
Text: message + description,
|
Text: message + description,
|
||||||
Sections: []Section{
|
}
|
||||||
{
|
if len(body.Title) == 0 {
|
||||||
ActivityTitle: "Condition results",
|
body.Title = "🚨 Gatus"
|
||||||
Text: results,
|
}
|
||||||
},
|
if len(formattedConditionResults) > 0 {
|
||||||
},
|
body.Sections = append(body.Sections, Section{
|
||||||
})
|
ActivityTitle: "Condition results",
|
||||||
return body
|
Text: formattedConditionResults,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||||
@@ -130,6 +137,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -143,6 +143,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
Resolved bool
|
Resolved bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
@@ -160,18 +161,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"@type\":\"MessageCard\",\"@context\":\"http://schema.org/extensions\",\"themeColor\":\"#36A64F\",\"title\":\"\\u0026#x1F6A8; Gatus\",\"text\":\"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row: description-2\",\"sections\":[{\"activityTitle\":\"Condition results\",\"text\":\"\\u0026#x2705; - `[CONNECTED] == true`\\u003cbr/\\u003e\\u0026#x2705; - `[STATUS] == 200`\\u003cbr/\\u003e\"}]}",
|
ExpectedBody: "{\"@type\":\"MessageCard\",\"@context\":\"http://schema.org/extensions\",\"themeColor\":\"#36A64F\",\"title\":\"\\u0026#x1F6A8; Gatus\",\"text\":\"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row: description-2\",\"sections\":[{\"activityTitle\":\"Condition results\",\"text\":\"\\u0026#x2705; - `[CONNECTED] == true`\\u003cbr/\\u003e\\u0026#x2705; - `[STATUS] == 200`\\u003cbr/\\u003e\"}]}",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: "{\"@type\":\"MessageCard\",\"@context\":\"http://schema.org/extensions\",\"themeColor\":\"#36A64F\",\"title\":\"\\u0026#x1F6A8; Gatus\",\"text\":\"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row: description-2\"}",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
}
|
||||||
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
ConditionResults: []*core.ConditionResult{
|
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
@@ -186,10 +197,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultAPIURL = "https://api.telegram.org"
|
const defaultAPIURL = "https://api.telegram.org"
|
||||||
@@ -36,8 +36,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
apiURL := provider.APIURL
|
apiURL := provider.APIURL
|
||||||
if apiURL == "" {
|
if apiURL == "" {
|
||||||
apiURL = defaultAPIURL
|
apiURL = defaultAPIURL
|
||||||
@@ -66,37 +66,41 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, results string
|
var message string
|
||||||
if resolved {
|
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.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} 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)
|
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
var formattedConditionResults string
|
||||||
var prefix string
|
if len(result.ConditionResults) > 0 {
|
||||||
if conditionResult.Success {
|
formattedConditionResults = "\n*Condition results*\n"
|
||||||
prefix = "✅"
|
for _, conditionResult := range result.ConditionResults {
|
||||||
} else {
|
var prefix string
|
||||||
prefix = "❌"
|
if conditionResult.Success {
|
||||||
|
prefix = "✅"
|
||||||
|
} else {
|
||||||
|
prefix = "❌"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
|
||||||
}
|
}
|
||||||
var text string
|
var text string
|
||||||
if len(alert.GetDescription()) > 0 {
|
if len(alert.GetDescription()) > 0 {
|
||||||
text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Description* \n_%s_ \n\n*Condition results*\n%s", message, alert.GetDescription(), results)
|
text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Description* \n_%s_ \n%s", message, alert.GetDescription(), formattedConditionResults)
|
||||||
} else {
|
} else {
|
||||||
text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Condition results*\n%s", message, results)
|
text = fmt.Sprintf("⛑ *Gatus* \n%s%s", message, formattedConditionResults)
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
bodyAsJSON, _ := json.Marshal(Body{
|
||||||
ChatID: provider.ID,
|
ChatID: provider.ID,
|
||||||
Text: text,
|
Text: text,
|
||||||
ParseMode: "MARKDOWN",
|
ParseMode: "MARKDOWN",
|
||||||
})
|
})
|
||||||
return body
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,10 +89,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -116,6 +116,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
Resolved bool
|
Resolved bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
@@ -133,18 +134,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Resolved: true,
|
Resolved: true,
|
||||||
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\"}",
|
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\"}",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
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 5 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\",\"parse_mode\":\"MARKDOWN\"}",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
}
|
||||||
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
ConditionResults: []*core.ConditionResult{
|
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
@@ -159,10 +170,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
||||||
@@ -30,8 +30,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -51,12 +51,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
return url.Values{
|
return url.Values{
|
||||||
"To": {provider.To},
|
"To": {provider.To},
|
||||||
@@ -66,6 +66,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
||||||
@@ -51,10 +51,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@@ -69,10 +69,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
t.Error("expected default alert to be not nil")
|
t.Error("expected default alert to be not nil")
|
||||||
}
|
}
|
||||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
static "github.com/TwiN/gatus/v5/web"
|
static "github.com/TwiN/gatus/v5/web"
|
||||||
"github.com/TwiN/health"
|
"github.com/TwiN/health"
|
||||||
fiber "github.com/gofiber/fiber/v2"
|
fiber "github.com/gofiber/fiber/v2"
|
||||||
@@ -26,6 +27,10 @@ type API struct {
|
|||||||
|
|
||||||
func New(cfg *config.Config) *API {
|
func New(cfg *config.Config) *API {
|
||||||
api := &API{}
|
api := &API{}
|
||||||
|
if cfg.Web == nil {
|
||||||
|
log.Println("[api.New] nil web config passed as parameter. This should only happen in tests. Using default web configuration")
|
||||||
|
cfg.Web = web.GetDefaultConfig()
|
||||||
|
}
|
||||||
api.router = api.createRouter(cfg)
|
api.router = api.createRouter(cfg)
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
@@ -40,6 +45,8 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
|
|||||||
log.Printf("[api.ErrorHandler] %s", err.Error())
|
log.Printf("[api.ErrorHandler] %s", err.Error())
|
||||||
return fiber.DefaultErrorHandler(c, err)
|
return fiber.DefaultErrorHandler(c, err)
|
||||||
},
|
},
|
||||||
|
ReadBufferSize: cfg.Web.ReadBufferSize,
|
||||||
|
Network: fiber.NetworkTCP,
|
||||||
})
|
})
|
||||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||||
app.Use(cors.New(cors.Config{
|
app.Use(cors.New(cors.Config{
|
||||||
@@ -65,9 +72,12 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
|
|||||||
unprotectedAPIRouter := apiRouter.Group("/")
|
unprotectedAPIRouter := apiRouter.Group("/")
|
||||||
unprotectedAPIRouter.Get("/v1/config", ConfigHandler{securityConfig: cfg.Security}.GetConfig)
|
unprotectedAPIRouter.Get("/v1/config", ConfigHandler{securityConfig: cfg.Security}.GetConfig)
|
||||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.svg", HealthBadge)
|
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.svg", HealthBadge)
|
||||||
|
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields)
|
||||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge)
|
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge)
|
||||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
|
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
|
||||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)
|
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)
|
||||||
|
// This endpoint requires authz with bearer token, so technically it is protected
|
||||||
|
unprotectedAPIRouter.Post("/v1/endpoints/:key/external", CreateExternalEndpointResult(cfg))
|
||||||
// SPA
|
// SPA
|
||||||
app.Get("/", SinglePageApplication(cfg.UI))
|
app.Get("/", SinglePageApplication(cfg.UI))
|
||||||
app.Get("/endpoints/:name", SinglePageApplication(cfg.UI))
|
app.Get("/endpoints/:name", SinglePageApplication(cfg.UI))
|
||||||
|
|||||||
+66
-10
@@ -1,12 +1,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
@@ -51,9 +54,9 @@ func UptimeBadge(c *fiber.Ctx) error {
|
|||||||
key := c.Params("key")
|
key := c.Params("key")
|
||||||
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
|
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrEndpointNotFound {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
return c.Status(404).SendString(err.Error())
|
return c.Status(404).SendString(err.Error())
|
||||||
} else if err == common.ErrInvalidTimeRange {
|
} else if errors.Is(err, common.ErrInvalidTimeRange) {
|
||||||
return c.Status(400).SendString(err.Error())
|
return c.Status(400).SendString(err.Error())
|
||||||
}
|
}
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
@@ -67,7 +70,7 @@ func UptimeBadge(c *fiber.Ctx) error {
|
|||||||
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||||
//
|
//
|
||||||
// Valid values for :duration -> 7d, 24h, 1h
|
// Valid values for :duration -> 7d, 24h, 1h
|
||||||
func ResponseTimeBadge(config *config.Config) fiber.Handler {
|
func ResponseTimeBadge(cfg *config.Config) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
duration := c.Params("duration")
|
duration := c.Params("duration")
|
||||||
var from time.Time
|
var from time.Time
|
||||||
@@ -84,9 +87,9 @@ func ResponseTimeBadge(config *config.Config) fiber.Handler {
|
|||||||
key := c.Params("key")
|
key := c.Params("key")
|
||||||
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrEndpointNotFound {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
return c.Status(404).SendString(err.Error())
|
return c.Status(404).SendString(err.Error())
|
||||||
} else if err == common.ErrInvalidTimeRange {
|
} else if errors.Is(err, common.ErrInvalidTimeRange) {
|
||||||
return c.Status(400).SendString(err.Error())
|
return c.Status(400).SendString(err.Error())
|
||||||
}
|
}
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
@@ -94,7 +97,7 @@ func ResponseTimeBadge(config *config.Config) fiber.Handler {
|
|||||||
c.Set("Content-Type", "image/svg+xml")
|
c.Set("Content-Type", "image/svg+xml")
|
||||||
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
c.Set("Expires", "0")
|
c.Set("Expires", "0")
|
||||||
return c.Status(200).Send(generateResponseTimeBadgeSVG(duration, averageResponseTime, key, config))
|
return c.Status(200).Send(generateResponseTimeBadgeSVG(duration, averageResponseTime, key, cfg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,9 +107,9 @@ func HealthBadge(c *fiber.Ctx) error {
|
|||||||
pagingConfig := paging.NewEndpointStatusParams()
|
pagingConfig := paging.NewEndpointStatusParams()
|
||||||
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrEndpointNotFound {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
return c.Status(404).SendString(err.Error())
|
return c.Status(404).SendString(err.Error())
|
||||||
} else if err == common.ErrInvalidTimeRange {
|
} else if errors.Is(err, common.ErrInvalidTimeRange) {
|
||||||
return c.Status(400).SendString(err.Error())
|
return c.Status(400).SendString(err.Error())
|
||||||
}
|
}
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
@@ -125,6 +128,36 @@ func HealthBadge(c *fiber.Ctx) error {
|
|||||||
return c.Status(200).Send(generateHealthBadgeSVG(healthStatus))
|
return c.Status(200).Send(generateHealthBadgeSVG(healthStatus))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HealthBadgeShields(c *fiber.Ctx) error {
|
||||||
|
key := c.Params("key")
|
||||||
|
pagingConfig := paging.NewEndpointStatusParams()
|
||||||
|
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
|
return c.Status(404).SendString(err.Error())
|
||||||
|
} else if errors.Is(err, common.ErrInvalidTimeRange) {
|
||||||
|
return c.Status(400).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
healthStatus := HealthStatusUnknown
|
||||||
|
if len(status.Results) > 0 {
|
||||||
|
if status.Results[0].Success {
|
||||||
|
healthStatus = HealthStatusUp
|
||||||
|
} else {
|
||||||
|
healthStatus = HealthStatusDown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Set("Content-Type", "application/json")
|
||||||
|
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
c.Set("Expires", "0")
|
||||||
|
jsonData, err := generateHealthBadgeShields(healthStatus)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
return c.Status(200).Send(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
||||||
var labelWidth, valueWidth, valueWidthAdjustment int
|
var labelWidth, valueWidth, valueWidthAdjustment int
|
||||||
switch duration {
|
switch duration {
|
||||||
@@ -240,10 +273,13 @@ func generateResponseTimeBadgeSVG(duration string, averageResponseTime int, key
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getBadgeColorFromResponseTime(responseTime int, key string, cfg *config.Config) string {
|
func getBadgeColorFromResponseTime(responseTime int, key string, cfg *config.Config) string {
|
||||||
endpoint := cfg.GetEndpointByKey(key)
|
thresholds := ui.GetDefaultConfig().Badge.ResponseTime.Thresholds
|
||||||
|
if endpoint := cfg.GetEndpointByKey(key); endpoint != nil {
|
||||||
|
thresholds = endpoint.UIConfig.Badge.ResponseTime.Thresholds
|
||||||
|
}
|
||||||
// the threshold config requires 5 values, so we can be sure it's set here
|
// the threshold config requires 5 values, so we can be sure it's set here
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
if responseTime <= endpoint.UIConfig.Badge.ResponseTime.Thresholds[i] {
|
if responseTime <= thresholds[i] {
|
||||||
return badgeColors[i]
|
return badgeColors[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,6 +335,17 @@ func generateHealthBadgeSVG(healthStatus string) []byte {
|
|||||||
return svg
|
return svg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateHealthBadgeShields(healthStatus string) ([]byte, error) {
|
||||||
|
color := getBadgeShieldsColorFromHealth(healthStatus)
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"label": "gatus",
|
||||||
|
"message": healthStatus,
|
||||||
|
"color": color,
|
||||||
|
}
|
||||||
|
return json.Marshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
func getBadgeColorFromHealth(healthStatus string) string {
|
func getBadgeColorFromHealth(healthStatus string) string {
|
||||||
if healthStatus == HealthStatusUp {
|
if healthStatus == HealthStatusUp {
|
||||||
return badgeColorHexAwesome
|
return badgeColorHexAwesome
|
||||||
@@ -307,3 +354,12 @@ func getBadgeColorFromHealth(healthStatus string) string {
|
|||||||
}
|
}
|
||||||
return badgeColorHexPassable
|
return badgeColorHexPassable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBadgeShieldsColorFromHealth(healthStatus string) string {
|
||||||
|
if healthStatus == HealthStatusUp {
|
||||||
|
return "brightgreen"
|
||||||
|
} else if healthStatus == HealthStatusDown {
|
||||||
|
return "red"
|
||||||
|
}
|
||||||
|
return "yellow"
|
||||||
|
}
|
||||||
|
|||||||
+30
-15
@@ -8,8 +8,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@@ -19,7 +19,7 @@ func TestBadge(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@@ -34,8 +34,8 @@ func TestBadge(t *testing.T) {
|
|||||||
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
||||||
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
||||||
|
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
@@ -110,6 +110,21 @@ func TestBadge(t *testing.T) {
|
|||||||
Path: "/api/v1/endpoints/invalid_key/health/badge.svg",
|
Path: "/api/v1/endpoints/invalid_key/health/badge.svg",
|
||||||
ExpectedCode: http.StatusNotFound,
|
ExpectedCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-shields-health-up",
|
||||||
|
Path: "/api/v1/endpoints/core_frontend/health/badge.shields",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-shields-health-down",
|
||||||
|
Path: "/api/v1/endpoints/core_backend/health/badge.shields",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badge-shields-health-for-invalid-key",
|
||||||
|
Path: "/api/v1/endpoints/invalid_key/health/badge.shields",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "chart-response-time-24h",
|
Name: "chart-response-time-24h",
|
||||||
Path: "/api/v1/endpoints/core_backend/response-times/24h/chart.svg",
|
Path: "/api/v1/endpoints/core_backend/response-times/24h/chart.svg",
|
||||||
@@ -203,30 +218,30 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
firstCondition = core.Condition("[STATUS] == 200")
|
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
)
|
)
|
||||||
|
|
||||||
firstTestEndpoint := core.Endpoint{
|
firstTestEndpoint := endpoint.Endpoint{
|
||||||
Name: "a",
|
Name: "a",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
UIConfig: ui.GetDefaultConfig(),
|
UIConfig: ui.GetDefaultConfig(),
|
||||||
}
|
}
|
||||||
secondTestEndpoint := core.Endpoint{
|
secondTestEndpoint := endpoint.Endpoint{
|
||||||
Name: "b",
|
Name: "b",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
@@ -240,10 +255,10 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
Endpoints: []*endpoint.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||||
}
|
}
|
||||||
|
|
||||||
testSuccessfulResult := core.Result{
|
testSuccessfulResult := endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
@@ -253,7 +268,7 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
|||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -42,9 +43,9 @@ func ResponseTimeChart(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(c.Params("key"), from, time.Now())
|
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(c.Params("key"), from, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrEndpointNotFound {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
return c.Status(404).SendString(err.Error())
|
return c.Status(404).SendString(err.Error())
|
||||||
} else if err == common.ErrInvalidTimeRange {
|
} else if errors.Is(err, common.ErrInvalidTimeRange) {
|
||||||
return c.Status(400).SendString(err.Error())
|
return c.Status(400).SendString(err.Error())
|
||||||
}
|
}
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
@@ -111,7 +112,7 @@ func ResponseTimeChart(c *fiber.Ctx) error {
|
|||||||
c.Set("Expires", "0")
|
c.Set("Expires", "0")
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
if err := graph.Render(chart.SVG, c); err != nil {
|
if err := graph.Render(chart.SVG, c); err != nil {
|
||||||
log.Println("[api][ResponseTimeChart] Failed to render response time chart:", err.Error())
|
log.Println("[api.ResponseTimeChart] Failed to render response time chart:", err.Error())
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+4
-4
@@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@@ -17,7 +17,7 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@@ -28,8 +28,8 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
|||||||
+15
-14
@@ -2,14 +2,15 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/remote"
|
"github.com/TwiN/gatus/v5/config/remote"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
@@ -26,19 +27,19 @@ func EndpointStatuses(cfg *config.Config) fiber.Handler {
|
|||||||
if !exists {
|
if !exists {
|
||||||
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
|
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[api][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
|
log.Printf("[api.EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
}
|
}
|
||||||
// ALPHA: Retrieve endpoint statuses from remote instances
|
// ALPHA: Retrieve endpoint statuses from remote instances
|
||||||
if endpointStatusesFromRemote, err := getEndpointStatusesFromRemoteInstances(cfg.Remote); err != nil {
|
if endpointStatusesFromRemote, err := getEndpointStatusesFromRemoteInstances(cfg.Remote); err != nil {
|
||||||
log.Printf("[handler][EndpointStatuses] Silently failed to retrieve endpoint statuses from remote: %s", err.Error())
|
log.Printf("[handler.EndpointStatuses] Silently failed to retrieve endpoint statuses from remote: %s", err.Error())
|
||||||
} else if endpointStatusesFromRemote != nil {
|
} else if endpointStatusesFromRemote != nil {
|
||||||
endpointStatuses = append(endpointStatuses, endpointStatusesFromRemote...)
|
endpointStatuses = append(endpointStatuses, endpointStatusesFromRemote...)
|
||||||
}
|
}
|
||||||
// Marshal endpoint statuses to JSON
|
// Marshal endpoint statuses to JSON
|
||||||
data, err = json.Marshal(endpointStatuses)
|
data, err = json.Marshal(endpointStatuses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[api][EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
|
log.Printf("[api.EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
|
||||||
return c.Status(500).SendString("unable to marshal object to JSON")
|
return c.Status(500).SendString("unable to marshal object to JSON")
|
||||||
}
|
}
|
||||||
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
|
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
|
||||||
@@ -50,11 +51,11 @@ func EndpointStatuses(cfg *config.Config) fiber.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*core.EndpointStatus, error) {
|
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*endpoint.Status, error) {
|
||||||
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var endpointStatusesFromAllRemotes []*core.EndpointStatus
|
var endpointStatusesFromAllRemotes []*endpoint.Status
|
||||||
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
||||||
for _, instance := range remoteConfig.Instances {
|
for _, instance := range remoteConfig.Instances {
|
||||||
response, err := httpClient.Get(instance.URL)
|
response, err := httpClient.Get(instance.URL)
|
||||||
@@ -64,13 +65,13 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
|
|||||||
body, err := io.ReadAll(response.Body)
|
body, err := io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = response.Body.Close()
|
_ = response.Body.Close()
|
||||||
log.Printf("[handler][getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var endpointStatuses []*core.EndpointStatus
|
var endpointStatuses []*endpoint.Status
|
||||||
if err = json.Unmarshal(body, &endpointStatuses); err != nil {
|
if err = json.Unmarshal(body, &endpointStatuses); err != nil {
|
||||||
_ = response.Body.Close()
|
_ = response.Body.Close()
|
||||||
log.Printf("[handler][getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_ = response.Body.Close()
|
_ = response.Body.Close()
|
||||||
@@ -82,24 +83,24 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
|
|||||||
return endpointStatusesFromAllRemotes, nil
|
return endpointStatusesFromAllRemotes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name
|
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name
|
||||||
func EndpointStatus(c *fiber.Ctx) error {
|
func EndpointStatus(c *fiber.Ctx) error {
|
||||||
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
||||||
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrEndpointNotFound {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
return c.Status(404).SendString(err.Error())
|
return c.Status(404).SendString(err.Error())
|
||||||
}
|
}
|
||||||
log.Printf("[api][EndpointStatus] Failed to retrieve endpoint status: %s", err.Error())
|
log.Printf("[api.EndpointStatus] Failed to retrieve endpoint status: %s", err.Error())
|
||||||
return c.Status(500).SendString(err.Error())
|
return c.Status(500).SendString(err.Error())
|
||||||
}
|
}
|
||||||
if endpointStatus == nil { // XXX: is this check necessary?
|
if endpointStatus == nil { // XXX: is this check necessary?
|
||||||
log.Printf("[api][EndpointStatus] Endpoint with key=%s not found", c.Params("key"))
|
log.Printf("[api.EndpointStatus] Endpoint with key=%s not found", c.Params("key"))
|
||||||
return c.Status(404).SendString("not found")
|
return c.Status(404).SendString("not found")
|
||||||
}
|
}
|
||||||
output, err := json.Marshal(endpointStatus)
|
output, err := json.Marshal(endpointStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[api][EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
|
log.Printf("[api.EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
|
||||||
return c.Status(500).SendString("unable to marshal object to JSON")
|
return c.Status(500).SendString("unable to marshal object to JSON")
|
||||||
}
|
}
|
||||||
c.Set("Content-Type", "application/json")
|
c.Set("Content-Type", "application/json")
|
||||||
|
|||||||
+10
-10
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@@ -16,19 +16,19 @@ import (
|
|||||||
var (
|
var (
|
||||||
timestamp = time.Now()
|
timestamp = time.Now()
|
||||||
|
|
||||||
testEndpoint = core.Endpoint{
|
testEndpoint = endpoint.Endpoint{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Group: "group",
|
Group: "group",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{core.Condition("[STATUS] == 200"), core.Condition("[RESPONSE_TIME] < 500"), core.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
Conditions: []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 500"), endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
}
|
}
|
||||||
testSuccessfulResult = core.Result{
|
testSuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
@@ -38,7 +38,7 @@ var (
|
|||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -53,7 +53,7 @@ var (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testUnsuccessfulResult = core.Result{
|
testUnsuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
@@ -63,7 +63,7 @@ var (
|
|||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Duration: 750 * time.Millisecond,
|
Duration: 750 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -85,7 +85,7 @@ func TestEndpointStatus(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@@ -96,8 +96,8 @@ func TestEndpointStatus(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Check if the success query parameter is present
|
||||||
|
success, exists := c.Queries()["success"]
|
||||||
|
if !exists || (success != "true" && success != "false") {
|
||||||
|
return c.Status(400).SendString("missing or invalid success query parameter")
|
||||||
|
}
|
||||||
|
// Check if the authorization bearer token header is correct
|
||||||
|
authorizationHeader := string(c.Request().Header.Peek("Authorization"))
|
||||||
|
if !strings.HasPrefix(authorizationHeader, "Bearer ") {
|
||||||
|
return c.Status(401).SendString("invalid Authorization header")
|
||||||
|
}
|
||||||
|
token := strings.TrimSpace(strings.TrimPrefix(authorizationHeader, "Bearer "))
|
||||||
|
if len(token) == 0 {
|
||||||
|
return c.Status(401).SendString("bearer token must not be empty")
|
||||||
|
}
|
||||||
|
key := c.Params("key")
|
||||||
|
externalEndpoint := cfg.GetExternalEndpointByKey(key)
|
||||||
|
if externalEndpoint == nil {
|
||||||
|
log.Printf("[api.CreateExternalEndpointResult] External endpoint with key=%s not found", key)
|
||||||
|
return c.Status(404).SendString("not found")
|
||||||
|
}
|
||||||
|
if externalEndpoint.Token != token {
|
||||||
|
log.Printf("[api.CreateExternalEndpointResult] Invalid token for external endpoint with key=%s", key)
|
||||||
|
return c.Status(401).SendString("invalid token")
|
||||||
|
}
|
||||||
|
// Persist the result in the storage
|
||||||
|
result := &endpoint.Result{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Success: c.QueryBool("success"),
|
||||||
|
Errors: []string{},
|
||||||
|
}
|
||||||
|
convertedEndpoint := externalEndpoint.ToEndpoint()
|
||||||
|
if err := store.Get().Insert(convertedEndpoint, result); err != nil {
|
||||||
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
|
return c.Status(404).SendString(err.Error())
|
||||||
|
}
|
||||||
|
log.Printf("[api.CreateExternalEndpointResult] Failed to insert result in storage: %s", err.Error())
|
||||||
|
return c.Status(500).SendString(err.Error())
|
||||||
|
}
|
||||||
|
log.Printf("[api.CreateExternalEndpointResult] Successfully inserted result for external endpoint with key=%s and success=%s", c.Params("key"), success)
|
||||||
|
// Check if an alert should be triggered or resolved
|
||||||
|
if !cfg.Maintenance.IsUnderMaintenance() {
|
||||||
|
watchdog.HandleAlerting(convertedEndpoint, result, cfg.Alerting, cfg.Debug)
|
||||||
|
externalEndpoint.NumberOfSuccessesInARow = convertedEndpoint.NumberOfSuccessesInARow
|
||||||
|
externalEndpoint.NumberOfFailuresInARow = convertedEndpoint.NumberOfFailuresInARow
|
||||||
|
}
|
||||||
|
// Return the result
|
||||||
|
return c.Status(200).SendString("")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||||
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateExternalEndpointResult(t *testing.T) {
|
||||||
|
defer store.Get().Clear()
|
||||||
|
defer cache.Clear()
|
||||||
|
cfg := &config.Config{
|
||||||
|
Alerting: &alerting.Config{
|
||||||
|
Discord: &discord.AlertProvider{},
|
||||||
|
},
|
||||||
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
||||||
|
{
|
||||||
|
Name: "n",
|
||||||
|
Group: "g",
|
||||||
|
Token: "token",
|
||||||
|
Alerts: []*alert.Alert{
|
||||||
|
{
|
||||||
|
Type: alert.TypeDiscord,
|
||||||
|
FailureThreshold: 2,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Maintenance: &maintenance.Config{},
|
||||||
|
}
|
||||||
|
api := New(cfg)
|
||||||
|
router := api.Router()
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
AuthorizationHeaderBearerToken string
|
||||||
|
ExpectedCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "no-token",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
||||||
|
AuthorizationHeaderBearerToken: "",
|
||||||
|
ExpectedCode: 401,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bad-token",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer bad-token",
|
||||||
|
ExpectedCode: 401,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bad-key",
|
||||||
|
Path: "/api/v1/endpoints/bad_key/external?success=true",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 404,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "bad-success-value",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=invalid",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "good-token-success-true",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "good-token-success-false",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=false",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "good-token-success-false-again",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=false",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
request := httptest.NewRequest("POST", scenario.Path, http.NoBody)
|
||||||
|
if len(scenario.AuthorizationHeaderBearerToken) > 0 {
|
||||||
|
request.Header.Set("Authorization", scenario.AuthorizationHeaderBearerToken)
|
||||||
|
}
|
||||||
|
response, err := router.Test(request)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != scenario.ExpectedCode {
|
||||||
|
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("verify-end-results", func(t *testing.T) {
|
||||||
|
endpointStatus, err := store.Get().GetEndpointStatus("g", "n", paging.NewEndpointStatusParams().WithResults(1, 10))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get endpoint status: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if endpointStatus.Key != "g_n" {
|
||||||
|
t.Errorf("expected key to be g_n but got %s", endpointStatus.Key)
|
||||||
|
}
|
||||||
|
if len(endpointStatus.Results) != 3 {
|
||||||
|
t.Errorf("expected 3 results but got %d", len(endpointStatus.Results))
|
||||||
|
}
|
||||||
|
if !endpointStatus.Results[0].Success {
|
||||||
|
t.Errorf("expected first result to be successful")
|
||||||
|
}
|
||||||
|
if endpointStatus.Results[1].Success {
|
||||||
|
t.Errorf("expected second result to be unsuccessful")
|
||||||
|
}
|
||||||
|
if endpointStatus.Results[2].Success {
|
||||||
|
t.Errorf("expected third result to be unsuccessful")
|
||||||
|
}
|
||||||
|
externalEndpointFromConfig := cfg.GetExternalEndpointByKey("g_n")
|
||||||
|
if externalEndpointFromConfig.NumberOfFailuresInARow != 2 {
|
||||||
|
t.Errorf("expected 2 failures in a row but got %d", externalEndpointFromConfig.NumberOfFailuresInARow)
|
||||||
|
}
|
||||||
|
if externalEndpointFromConfig.NumberOfSuccessesInARow != 0 {
|
||||||
|
t.Errorf("expected 0 successes in a row but got %d", externalEndpointFromConfig.NumberOfSuccessesInARow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+2
-2
@@ -15,14 +15,14 @@ func SinglePageApplication(ui *ui.Config) fiber.Handler {
|
|||||||
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||||
log.Println("[api][SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error:", err.Error())
|
log.Println("[api.SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||||
return c.Status(500).SendString("Failed to parse template. This should never happen, because the template is validated on start.")
|
return c.Status(500).SendString("Failed to parse template. This should never happen, because the template is validated on start.")
|
||||||
}
|
}
|
||||||
c.Set("Content-Type", "text/html")
|
c.Set("Content-Type", "text/html")
|
||||||
err = t.Execute(c, ui)
|
err = t.Execute(c, ui)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||||
log.Println("[api][SinglePageApplication] Failed to execute template. This should never happen, because the template is validated on start. Error:", err.Error())
|
log.Println("[api.SinglePageApplication] Failed to execute template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||||
return c.Status(500).SendString("Failed to parse template. This should never happen, because the template is validated on start.")
|
return c.Status(500).SendString("Failed to parse template. This should never happen, because the template is validated on start.")
|
||||||
}
|
}
|
||||||
return c.SendStatus(200)
|
return c.SendStatus(200)
|
||||||
|
|||||||
+4
-4
@@ -9,8 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/ui"
|
"github.com/TwiN/gatus/v5/config/ui"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@@ -20,7 +20,7 @@ func TestSinglePageApplication(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@@ -34,8 +34,8 @@ func TestSinglePageApplication(t *testing.T) {
|
|||||||
Title: "example-title",
|
Title: "example-title",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
|||||||
+51
-5
@@ -16,11 +16,16 @@ import (
|
|||||||
"github.com/TwiN/gocache/v2"
|
"github.com/TwiN/gocache/v2"
|
||||||
"github.com/TwiN/whois"
|
"github.com/TwiN/whois"
|
||||||
"github.com/ishidawataru/sctp"
|
"github.com/ishidawataru/sctp"
|
||||||
|
"github.com/miekg/dns"
|
||||||
ping "github.com/prometheus-community/pro-bing"
|
ping "github.com/prometheus-community/pro-bing"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsPort = 53
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// injectedHTTPClient is used for testing purposes
|
// injectedHTTPClient is used for testing purposes
|
||||||
injectedHTTPClient *http.Client
|
injectedHTTPClient *http.Client
|
||||||
@@ -235,10 +240,7 @@ func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool
|
|||||||
//
|
//
|
||||||
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
|
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
|
||||||
func Ping(address string, config *Config) (bool, time.Duration) {
|
func Ping(address string, config *Config) (bool, time.Duration) {
|
||||||
pinger, err := ping.NewPinger(address)
|
pinger := ping.New(address)
|
||||||
if err != nil {
|
|
||||||
return false, 0
|
|
||||||
}
|
|
||||||
pinger.Count = 1
|
pinger.Count = 1
|
||||||
pinger.Timeout = config.Timeout
|
pinger.Timeout = config.Timeout
|
||||||
// Set the pinger's privileged mode to true for every GOOS except darwin
|
// Set the pinger's privileged mode to true for every GOOS except darwin
|
||||||
@@ -247,7 +249,8 @@ func Ping(address string, config *Config) (bool, time.Duration) {
|
|||||||
// Note that for this to work on Linux, Gatus must run with sudo privileges.
|
// Note that for this to work on Linux, Gatus must run with sudo privileges.
|
||||||
// See https://github.com/prometheus-community/pro-bing#linux
|
// See https://github.com/prometheus-community/pro-bing#linux
|
||||||
pinger.SetPrivileged(runtime.GOOS != "darwin")
|
pinger.SetPrivileged(runtime.GOOS != "darwin")
|
||||||
err = pinger.Run()
|
pinger.SetNetwork(config.Network)
|
||||||
|
err := pinger.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, 0
|
return false, 0
|
||||||
}
|
}
|
||||||
@@ -293,6 +296,49 @@ func QueryWebSocket(address, body string, config *Config) (bool, []byte, error)
|
|||||||
return true, msg[:n], nil
|
return true, msg[:n], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string, body []byte, err error) {
|
||||||
|
if !strings.Contains(url, ":") {
|
||||||
|
url = fmt.Sprintf("%s:%d", url, dnsPort)
|
||||||
|
}
|
||||||
|
queryTypeAsUint16 := dns.StringToType[queryType]
|
||||||
|
c := new(dns.Client)
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(queryName, queryTypeAsUint16)
|
||||||
|
r, _, err := c.Exchange(m, url)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", nil, err
|
||||||
|
}
|
||||||
|
connected = true
|
||||||
|
dnsRcode = dns.RcodeToString[r.Rcode]
|
||||||
|
for _, rr := range r.Answer {
|
||||||
|
switch rr.Header().Rrtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
if a, ok := rr.(*dns.A); ok {
|
||||||
|
body = []byte(a.A.String())
|
||||||
|
}
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||||
|
body = []byte(aaaa.AAAA.String())
|
||||||
|
}
|
||||||
|
case dns.TypeCNAME:
|
||||||
|
if cname, ok := rr.(*dns.CNAME); ok {
|
||||||
|
body = []byte(cname.Target)
|
||||||
|
}
|
||||||
|
case dns.TypeMX:
|
||||||
|
if mx, ok := rr.(*dns.MX); ok {
|
||||||
|
body = []byte(mx.Mx)
|
||||||
|
}
|
||||||
|
case dns.TypeNS:
|
||||||
|
if ns, ok := rr.(*dns.NS); ok {
|
||||||
|
body = []byte(ns.Ns)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
body = []byte("query type is not supported yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connected, dnsRcode, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
||||||
func InjectHTTPClient(httpClient *http.Client) {
|
func InjectHTTPClient(httpClient *http.Client) {
|
||||||
injectedHTTPClient = httpClient
|
injectedHTTPClient = httpClient
|
||||||
|
|||||||
+167
-1
@@ -2,11 +2,14 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
|
"github.com/TwiN/gatus/v5/pattern"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,6 +86,32 @@ func TestPing(t *testing.T) {
|
|||||||
t.Error("Round-trip time returned on failure should've been 0")
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Can't perform integration tests (e.g. pinging public targets by single-stacked hostname) here,
|
||||||
|
// because ICMP is blocked in the network of GitHub-hosted runners.
|
||||||
|
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
|
||||||
|
t.Error("expected true")
|
||||||
|
if rtt == 0 {
|
||||||
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
|
||||||
|
t.Error("expected true")
|
||||||
|
if rtt == 0 {
|
||||||
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip4"}); success {
|
||||||
|
t.Error("expected false, because the IP isn't an IPv4 address")
|
||||||
|
if rtt != 0 {
|
||||||
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip6"}); success {
|
||||||
|
t.Error("expected false, because the IP isn't an IPv6 address")
|
||||||
|
if rtt != 0 {
|
||||||
|
t.Error("Round-trip time returned on failure should've been 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanPerformStartTLS(t *testing.T) {
|
func TestCanPerformStartTLS(t *testing.T) {
|
||||||
@@ -250,7 +279,7 @@ func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
|
|||||||
// to us as `X-Org-Authorization` header, we check here if the value matches
|
// to us as `X-Org-Authorization` header, we check here if the value matches
|
||||||
// our expected token `secret-token`
|
// our expected token `secret-token`
|
||||||
if response.Header.Get("X-Org-Authorization") != "Bearer secret-token" {
|
if response.Header.Get("X-Org-Authorization") != "Bearer secret-token" {
|
||||||
t.Error("exptected `secret-token` as Bearer token in the mocked response header `X-Org-Authorization`, but got", response.Header.Get("X-Org-Authorization"))
|
t.Error("expected `secret-token` as Bearer token in the mocked response header `X-Org-Authorization`, but got", response.Header.Get("X-Org-Authorization"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,3 +293,140 @@ func TestQueryWebSocket(t *testing.T) {
|
|||||||
t.Error("expected an error due to the target not being websocket-friendly")
|
t.Error("expected an error due to the target not being websocket-friendly")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTlsRenegotiation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg TLSConfig
|
||||||
|
expectedConfig tls.RenegotiationSupport
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
cfg: TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||||
|
expectedConfig: tls.RenegotiateNever,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "never",
|
||||||
|
cfg: TLSConfig{RenegotiationSupport: "never", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||||
|
expectedConfig: tls.RenegotiateNever,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "once",
|
||||||
|
cfg: TLSConfig{RenegotiationSupport: "once", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||||
|
expectedConfig: tls.RenegotiateOnceAsClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "freely",
|
||||||
|
cfg: TLSConfig{RenegotiationSupport: "freely", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||||
|
expectedConfig: tls.RenegotiateFreelyAsClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not-valid-and-broken",
|
||||||
|
cfg: TLSConfig{RenegotiationSupport: "invalid", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||||
|
expectedConfig: tls.RenegotiateNever,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
tls := &tls.Config{}
|
||||||
|
tlsConfig := configureTLS(tls, test.cfg)
|
||||||
|
if tlsConfig.Renegotiation != test.expectedConfig {
|
||||||
|
t.Errorf("expected tls renegotiation to be %v, but got %v", test.expectedConfig, tls.Renegotiation)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryDNS(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputDNS dns.Config
|
||||||
|
inputURL string
|
||||||
|
expectedDNSCode string
|
||||||
|
expectedBody string
|
||||||
|
isErrExpected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test Config with type A",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "93.184.215.14",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type AAAA",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "AAAA",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "2606:2800:21f:cb07:6820:80da:af6b:8b2c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type CNAME",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "CNAME",
|
||||||
|
QueryName: "en.wikipedia.org.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "dyna.wikimedia.org.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type MX",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "MX",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: ".",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type NS",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "NS",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "*.iana-servers.net.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with fake type and retrieve error",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "B",
|
||||||
|
QueryName: "example",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
isErrExpected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, dnsRCode, body, err := QueryDNS(test.inputDNS.QueryType, test.inputDNS.QueryName, test.inputURL)
|
||||||
|
if test.isErrExpected && err == nil {
|
||||||
|
t.Errorf("there should be an error")
|
||||||
|
}
|
||||||
|
if dnsRCode != test.expectedDNSCode {
|
||||||
|
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, dnsRCode)
|
||||||
|
}
|
||||||
|
if test.inputDNS.QueryType == "NS" {
|
||||||
|
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
||||||
|
if !pattern.Match(test.expectedBody, string(body)) {
|
||||||
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if string(body) != test.expectedBody {
|
||||||
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+151
-6
@@ -7,12 +7,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/clientcredentials"
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
|
"google.golang.org/api/idtoken"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,11 +25,14 @@ var (
|
|||||||
ErrInvalidDNSResolver = errors.New("invalid DNS resolver specified. Required format is {proto}://{ip}:{port}")
|
ErrInvalidDNSResolver = errors.New("invalid DNS resolver specified. Required format is {proto}://{ip}:{port}")
|
||||||
ErrInvalidDNSResolverPort = errors.New("invalid DNS resolver port")
|
ErrInvalidDNSResolverPort = errors.New("invalid DNS resolver port")
|
||||||
ErrInvalidClientOAuth2Config = errors.New("invalid oauth2 configuration: must define all fields for client credentials flow (token-url, client-id, client-secret, scopes)")
|
ErrInvalidClientOAuth2Config = errors.New("invalid oauth2 configuration: must define all fields for client credentials flow (token-url, client-id, client-secret, scopes)")
|
||||||
|
ErrInvalidClientIAPConfig = errors.New("invalid Identity-Aware-Proxy configuration: must define all fields for Google Identity-Aware-Proxy programmatic authentication (audience)")
|
||||||
|
ErrInvalidClientTLSConfig = errors.New("invalid TLS configuration: certificate-file and private-key-file must be specified")
|
||||||
|
|
||||||
defaultConfig = Config{
|
defaultConfig = Config{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
IgnoreRedirect: false,
|
IgnoreRedirect: false,
|
||||||
Timeout: defaultTimeout,
|
Timeout: defaultTimeout,
|
||||||
|
Network: "ip",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,6 +44,9 @@ func GetDefaultConfig() *Config {
|
|||||||
|
|
||||||
// Config is the configuration for clients
|
// Config is the configuration for clients
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// ProxyURL is the URL of the proxy to use for the client
|
||||||
|
ProxyURL string `yaml:"proxy-url,omitempty"`
|
||||||
|
|
||||||
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
||||||
Insecure bool `yaml:"insecure,omitempty"`
|
Insecure bool `yaml:"insecure,omitempty"`
|
||||||
|
|
||||||
@@ -58,7 +66,16 @@ type Config struct {
|
|||||||
// See configureOAuth2 for more details.
|
// See configureOAuth2 for more details.
|
||||||
OAuth2Config *OAuth2Config `yaml:"oauth2,omitempty"`
|
OAuth2Config *OAuth2Config `yaml:"oauth2,omitempty"`
|
||||||
|
|
||||||
|
// IAPConfig is the Google Cloud Identity-Aware-Proxy configuration used for the client. (e.g. audience)
|
||||||
|
IAPConfig *IAPConfig `yaml:"identity-aware-proxy,omitempty"`
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
|
// Network (ip, ip4 or ip6) for the ICMP client
|
||||||
|
Network string `yaml:"network"`
|
||||||
|
|
||||||
|
// TLS configuration (optional)
|
||||||
|
TLS *TLSConfig `yaml:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
|
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
|
||||||
@@ -76,6 +93,22 @@ type OAuth2Config struct {
|
|||||||
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
|
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IAPConfig is the configuration for the Google Cloud Identity-Aware-Proxy
|
||||||
|
type IAPConfig struct {
|
||||||
|
Audience string `yaml:"audience"` // e.g. "toto.apps.googleusercontent.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSConfig is the configuration for mTLS configurations
|
||||||
|
type TLSConfig struct {
|
||||||
|
// CertificateFile is the public certificate for TLS in PEM format.
|
||||||
|
CertificateFile string `yaml:"certificate-file,omitempty"`
|
||||||
|
|
||||||
|
// PrivateKeyFile is the private key file for TLS in PEM format.
|
||||||
|
PrivateKeyFile string `yaml:"private-key-file,omitempty"`
|
||||||
|
|
||||||
|
RenegotiationSupport string `yaml:"renegotiation,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
|
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
|
||||||
func (c *Config) ValidateAndSetDefaults() error {
|
func (c *Config) ValidateAndSetDefaults() error {
|
||||||
if c.Timeout < time.Millisecond {
|
if c.Timeout < time.Millisecond {
|
||||||
@@ -90,6 +123,14 @@ func (c *Config) ValidateAndSetDefaults() error {
|
|||||||
if c.HasOAuth2Config() && !c.OAuth2Config.isValid() {
|
if c.HasOAuth2Config() && !c.OAuth2Config.isValid() {
|
||||||
return ErrInvalidClientOAuth2Config
|
return ErrInvalidClientOAuth2Config
|
||||||
}
|
}
|
||||||
|
if c.HasIAPConfig() && !c.IAPConfig.isValid() {
|
||||||
|
return ErrInvalidClientIAPConfig
|
||||||
|
}
|
||||||
|
if c.HasTlsConfig() {
|
||||||
|
if err := c.TLS.isValid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,13 +171,46 @@ func (c *Config) HasOAuth2Config() bool {
|
|||||||
return c.OAuth2Config != nil
|
return c.OAuth2Config != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasIAPConfig returns true if the client has IAP configuration parameters
|
||||||
|
func (c *Config) HasIAPConfig() bool {
|
||||||
|
return c.IAPConfig != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTlsConfig returns true if the client has client certificate parameters
|
||||||
|
func (c *Config) HasTlsConfig() bool {
|
||||||
|
return c.TLS != nil && len(c.TLS.CertificateFile) > 0 && len(c.TLS.PrivateKeyFile) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValid() returns true if the IAP configuration is valid
|
||||||
|
func (c *IAPConfig) isValid() bool {
|
||||||
|
return len(c.Audience) > 0
|
||||||
|
}
|
||||||
|
|
||||||
// isValid() returns true if the OAuth2 configuration is valid
|
// isValid() returns true if the OAuth2 configuration is valid
|
||||||
func (c *OAuth2Config) isValid() bool {
|
func (c *OAuth2Config) isValid() bool {
|
||||||
return len(c.TokenURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0
|
return len(c.TokenURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValid() returns nil if the client tls certificates are valid, otherwise returns an error
|
||||||
|
func (t *TLSConfig) isValid() error {
|
||||||
|
if len(t.CertificateFile) > 0 && len(t.PrivateKeyFile) > 0 {
|
||||||
|
_, err := tls.LoadX509KeyPair(t.CertificateFile, t.PrivateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ErrInvalidClientTLSConfig
|
||||||
|
}
|
||||||
|
|
||||||
// GetHTTPClient return an HTTP client matching the Config's parameters.
|
// GetHTTPClient return an HTTP client matching the Config's parameters.
|
||||||
func (c *Config) getHTTPClient() *http.Client {
|
func (c *Config) getHTTPClient() *http.Client {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: c.Insecure,
|
||||||
|
}
|
||||||
|
if c.HasTlsConfig() && c.TLS.isValid() == nil {
|
||||||
|
tlsConfig = configureTLS(tlsConfig, *c.TLS)
|
||||||
|
}
|
||||||
if c.httpClient == nil {
|
if c.httpClient == nil {
|
||||||
c.httpClient = &http.Client{
|
c.httpClient = &http.Client{
|
||||||
Timeout: c.Timeout,
|
Timeout: c.Timeout,
|
||||||
@@ -144,9 +218,7 @@ func (c *Config) getHTTPClient() *http.Client {
|
|||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
MaxIdleConnsPerHost: 20,
|
MaxIdleConnsPerHost: 20,
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: tlsConfig,
|
||||||
InsecureSkipVerify: c.Insecure,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
if c.IgnoreRedirect {
|
if c.IgnoreRedirect {
|
||||||
@@ -157,12 +229,20 @@ func (c *Config) getHTTPClient() *http.Client {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if c.ProxyURL != "" {
|
||||||
|
proxyURL, err := url.Parse(c.ProxyURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[client.getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring custom proxy due to error:", err.Error())
|
||||||
|
} else {
|
||||||
|
c.httpClient.Transport.(*http.Transport).Proxy = http.ProxyURL(proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.HasCustomDNSResolver() {
|
if c.HasCustomDNSResolver() {
|
||||||
dnsResolver, err := c.parseDNSResolver()
|
dnsResolver, err := c.parseDNSResolver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// We're ignoring the error, because it should have been validated on startup ValidateAndSetDefaults.
|
// We're ignoring the error, because it should have been validated on startup ValidateAndSetDefaults.
|
||||||
// It shouldn't happen, but if it does, we'll log it... Better safe than sorry ;)
|
// It shouldn't happen, but if it does, we'll log it... Better safe than sorry ;)
|
||||||
log.Println("[client][getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring invalid DNS resolver due to error:", err.Error())
|
log.Println("[client.getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring invalid DNS resolver due to error:", err.Error())
|
||||||
} else {
|
} else {
|
||||||
dialer := &net.Dialer{
|
dialer := &net.Dialer{
|
||||||
Resolver: &net.Resolver{
|
Resolver: &net.Resolver{
|
||||||
@@ -178,13 +258,56 @@ func (c *Config) getHTTPClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.HasOAuth2Config() {
|
if c.HasOAuth2Config() && c.HasIAPConfig() {
|
||||||
|
log.Println("[client.getHTTPClient] Error: Both Identity-Aware-Proxy and Oauth2 configuration are present.")
|
||||||
|
} else if c.HasOAuth2Config() {
|
||||||
c.httpClient = configureOAuth2(c.httpClient, *c.OAuth2Config)
|
c.httpClient = configureOAuth2(c.httpClient, *c.OAuth2Config)
|
||||||
|
} else if c.HasIAPConfig() {
|
||||||
|
c.httpClient = configureIAP(c.httpClient, *c.IAPConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.httpClient
|
return c.httpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateIAPToken returns a boolean that will define if the google identity-aware-proxy token can be fetch
|
||||||
|
// and if is it valid.
|
||||||
|
func validateIAPToken(ctx context.Context, c IAPConfig) bool {
|
||||||
|
ts, err := idtoken.NewTokenSource(ctx, c.Audience)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[client.ValidateIAPToken] Claiming Identity token failed. error:", err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tok, err := ts.Token()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[client.ValidateIAPToken] Get Identity-Aware-Proxy token failed. error:", err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
payload, err := idtoken.Validate(ctx, tok.AccessToken, c.Audience)
|
||||||
|
_ = payload
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[client.ValidateIAPToken] Token Validation failed. error:", err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureIAP returns an HTTP client that will obtain and refresh Identity-Aware-Proxy tokens as necessary.
|
||||||
|
// The returned Client and its Transport should not be modified.
|
||||||
|
func configureIAP(httpClient *http.Client, c IAPConfig) *http.Client {
|
||||||
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||||
|
if validateIAPToken(ctx, c) {
|
||||||
|
ts, err := idtoken.NewTokenSource(ctx, c.Audience)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[client.ConfigureIAP] Claiming Token Source failed. error:", err.Error())
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
client := oauth2.NewClient(ctx, ts)
|
||||||
|
client.Timeout = httpClient.Timeout
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
|
||||||
// configureOAuth2 returns an HTTP client that will obtain and refresh tokens as necessary.
|
// configureOAuth2 returns an HTTP client that will obtain and refresh tokens as necessary.
|
||||||
// The returned Client and its Transport should not be modified.
|
// The returned Client and its Transport should not be modified.
|
||||||
func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
|
func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
|
||||||
@@ -195,5 +318,27 @@ func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
|
|||||||
TokenURL: c.TokenURL,
|
TokenURL: c.TokenURL,
|
||||||
}
|
}
|
||||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||||
return oauth2cfg.Client(ctx)
|
client := oauth2cfg.Client(ctx)
|
||||||
|
client.Timeout = httpClient.Timeout
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureTLS returns a TLS Config that will enable mTLS
|
||||||
|
func configureTLS(tlsConfig *tls.Config, c TLSConfig) *tls.Config {
|
||||||
|
clientTLSCert, err := tls.LoadX509KeyPair(c.CertificateFile, c.PrivateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{clientTLSCert}
|
||||||
|
tlsConfig.Renegotiation = tls.RenegotiateNever
|
||||||
|
|
||||||
|
renegotionSupport := map[string]tls.RenegotiationSupport{
|
||||||
|
"once": tls.RenegotiateOnceAsClient,
|
||||||
|
"freely": tls.RenegotiateFreelyAsClient,
|
||||||
|
"never": tls.RenegotiateNever,
|
||||||
|
}
|
||||||
|
if val, ok := renegotionSupport[c.RenegotiationSupport]; ok {
|
||||||
|
tlsConfig.Renegotiation = val
|
||||||
|
}
|
||||||
|
return tlsConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -79,3 +80,92 @@ func TestConfig_ValidateAndSetDefaults_withCustomDNSResolver(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_getHTTPClient_withCustomProxyURL(t *testing.T) {
|
||||||
|
proxyURL := "http://proxy.example.com:8080"
|
||||||
|
cfg := &Config{
|
||||||
|
ProxyURL: proxyURL,
|
||||||
|
}
|
||||||
|
cfg.ValidateAndSetDefaults()
|
||||||
|
client := cfg.getHTTPClient()
|
||||||
|
transport := client.Transport.(*http.Transport)
|
||||||
|
if transport.Proxy == nil {
|
||||||
|
t.Errorf("expected Config.ProxyURL to set the HTTP client's proxy to %s", proxyURL)
|
||||||
|
}
|
||||||
|
req := &http.Request{
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "www.example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectProxyURL, err := transport.Proxy(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("can't proxy the request %s", proxyURL)
|
||||||
|
}
|
||||||
|
if proxyURL != expectProxyURL.String() {
|
||||||
|
t.Errorf("expected Config.ProxyURL to set the HTTP client's proxy to %s", proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_TlsIsValid(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg *Config
|
||||||
|
expectedErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "good-tls-config",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"}},
|
||||||
|
expectedErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-certificate-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "doesnotexist", PrivateKeyFile: "../testdata/cert.key"}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad-certificate-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/badcert.pem", PrivateKeyFile: "../testdata/cert.key"}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-certificate-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "", PrivateKeyFile: "../testdata/cert.key"}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-private-key-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "doesnotexist"}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-private-key-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: ""}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad-private-key-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/badcert.key"}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad-certificate-and-private-key-file",
|
||||||
|
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/badcert.pem", PrivateKeyFile: "../testdata/badcert.key"}},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
err := test.cfg.TLS.isValid()
|
||||||
|
if (err != nil) != test.expectedErr {
|
||||||
|
t.Errorf("expected the existence of an error to be %v, got %v", test.expectedErr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !test.expectedErr {
|
||||||
|
if test.cfg.TLS.isValid() != nil {
|
||||||
|
t.Error("cfg.TLS.isValid() returned an error even though no error was expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ endpoints:
|
|||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
query-type: "A"
|
query-type: "A"
|
||||||
conditions:
|
conditions:
|
||||||
- "[BODY] == 93.184.216.34"
|
- "[BODY] == 93.184.215.14"
|
||||||
- "[DNS_RCODE] == NOERROR"
|
- "[DNS_RCODE] == NOERROR"
|
||||||
|
|
||||||
- name: icmp-ping
|
- name: icmp-ping
|
||||||
|
|||||||
+76
-32
@@ -15,14 +15,13 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||||
"github.com/TwiN/gatus/v5/config/remote"
|
"github.com/TwiN/gatus/v5/config/remote"
|
||||||
"github.com/TwiN/gatus/v5/config/ui"
|
"github.com/TwiN/gatus/v5/config/ui"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/security"
|
"github.com/TwiN/gatus/v5/security"
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,14 +66,17 @@ type Config struct {
|
|||||||
// Disabling this may lead to inaccurate response times
|
// Disabling this may lead to inaccurate response times
|
||||||
DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"`
|
DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"`
|
||||||
|
|
||||||
// Security Configuration for securing access to Gatus
|
// Security is the configuration for securing access to Gatus
|
||||||
Security *security.Config `yaml:"security,omitempty"`
|
Security *security.Config `yaml:"security,omitempty"`
|
||||||
|
|
||||||
// Alerting Configuration for alerting
|
// Alerting is the configuration for alerting providers
|
||||||
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
||||||
|
|
||||||
// Endpoints List of endpoints to monitor
|
// Endpoints is the list of endpoints to monitor
|
||||||
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"`
|
Endpoints []*endpoint.Endpoint `yaml:"endpoints,omitempty"`
|
||||||
|
|
||||||
|
// ExternalEndpoints is the list of all external endpoints
|
||||||
|
ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
||||||
|
|
||||||
// Storage is the configuration for how the data is stored
|
// Storage is the configuration for how the data is stored
|
||||||
Storage *storage.Config `yaml:"storage,omitempty"`
|
Storage *storage.Config `yaml:"storage,omitempty"`
|
||||||
@@ -99,20 +101,29 @@ type Config struct {
|
|||||||
lastFileModTime time.Time // last modification time
|
lastFileModTime time.Time // last modification time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
|
func (config *Config) GetEndpointByKey(key string) *endpoint.Endpoint {
|
||||||
// TODO: Should probably add a mutex here to prevent concurrent access
|
|
||||||
for i := 0; i < len(config.Endpoints); i++ {
|
for i := 0; i < len(config.Endpoints); i++ {
|
||||||
ep := config.Endpoints[i]
|
ep := config.Endpoints[i]
|
||||||
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
|
if ep.Key() == key {
|
||||||
return ep
|
return ep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) GetExternalEndpointByKey(key string) *endpoint.ExternalEndpoint {
|
||||||
|
for i := 0; i < len(config.ExternalEndpoints); i++ {
|
||||||
|
ee := config.ExternalEndpoints[i]
|
||||||
|
if ee.Key() == key {
|
||||||
|
return ee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// HasLoadedConfigurationBeenModified returns whether one of the file that the
|
// HasLoadedConfigurationBeenModified returns whether one of the file that the
|
||||||
// configuration has been loaded from has been modified since it was last read
|
// configuration has been loaded from has been modified since it was last read
|
||||||
func (config Config) HasLoadedConfigurationBeenModified() bool {
|
func (config *Config) HasLoadedConfigurationBeenModified() bool {
|
||||||
lastMod := config.lastFileModTime.Unix()
|
lastMod := config.lastFileModTime.Unix()
|
||||||
fileInfo, err := os.Stat(config.configPath)
|
fileInfo, err := os.Stat(config.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,7 +136,7 @@ func (config Config) HasLoadedConfigurationBeenModified() bool {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return err == errEarlyReturn
|
return errors.Is(err, errEarlyReturn)
|
||||||
}
|
}
|
||||||
return !fileInfo.ModTime().IsZero() && config.lastFileModTime.Unix() < fileInfo.ModTime().Unix()
|
return !fileInfo.ModTime().IsZero() && config.lastFileModTime.Unix() < fileInfo.ModTime().Unix()
|
||||||
}
|
}
|
||||||
@@ -135,7 +146,7 @@ func (config *Config) UpdateLastFileModTime() {
|
|||||||
config.lastFileModTime = time.Now()
|
config.lastFileModTime = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfiguration loads the full configuration composed from the main configuration file
|
// LoadConfiguration loads the full configuration composed of the main configuration file
|
||||||
// and all composed configuration files
|
// and all composed configuration files
|
||||||
func LoadConfiguration(configPath string) (*Config, error) {
|
func LoadConfiguration(configPath string) (*Config, error) {
|
||||||
var configBytes []byte
|
var configBytes []byte
|
||||||
@@ -161,13 +172,13 @@ func LoadConfiguration(configPath string) (*Config, error) {
|
|||||||
if fileInfo.IsDir() {
|
if fileInfo.IsDir() {
|
||||||
err := walkConfigDir(configPath, func(path string, d fs.DirEntry, err error) error {
|
err := walkConfigDir(configPath, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[config][LoadConfiguration] Error walking path=%s: %s", path, err)
|
log.Printf("[config.LoadConfiguration] Error walking path=%s: %s", path, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("[config][LoadConfiguration] Reading configuration from %s", path)
|
log.Printf("[config.LoadConfiguration] Reading configuration from %s", path)
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[config][LoadConfiguration] Error reading configuration from %s: %s", path, err)
|
log.Printf("[config.LoadConfiguration] Error reading configuration from %s: %s", path, err)
|
||||||
return fmt.Errorf("error reading configuration from file %s: %w", path, err)
|
return fmt.Errorf("error reading configuration from file %s: %w", path, err)
|
||||||
}
|
}
|
||||||
configBytes, err = deepmerge.YAML(configBytes, data)
|
configBytes, err = deepmerge.YAML(configBytes, data)
|
||||||
@@ -177,7 +188,7 @@ func LoadConfiguration(configPath string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("error reading configuration from directory %s: %w", usedConfigPath, err)
|
return nil, fmt.Errorf("error reading configuration from directory %s: %w", usedConfigPath, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[config][LoadConfiguration] Reading configuration from configFile=%s", configPath)
|
log.Printf("[config.LoadConfiguration] Reading configuration from configFile=%s", configPath)
|
||||||
if data, err := os.ReadFile(usedConfigPath); err != nil {
|
if data, err := os.ReadFile(usedConfigPath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
@@ -234,7 +245,7 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
if config == nil || config.Endpoints == nil || len(config.Endpoints) == 0 {
|
if config == nil || config.Endpoints == nil || len(config.Endpoints) == 0 {
|
||||||
err = ErrNoEndpointInConfig
|
err = ErrNoEndpointInConfig
|
||||||
} else {
|
} else {
|
||||||
validateAlertingConfig(config.Alerting, config.Endpoints, config.Debug)
|
validateAlertingConfig(config.Alerting, config.Endpoints, config.ExternalEndpoints, config.Debug)
|
||||||
if err := validateSecurityConfig(config); err != nil {
|
if err := validateSecurityConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -324,15 +335,37 @@ func validateWebConfig(config *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointsConfig(config *Config) error {
|
func validateEndpointsConfig(config *Config) error {
|
||||||
for _, endpoint := range config.Endpoints {
|
duplicateValidationMap := make(map[string]bool)
|
||||||
|
// Validate endpoints
|
||||||
|
for _, ep := range config.Endpoints {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config][validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name)
|
log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", ep.Name)
|
||||||
}
|
}
|
||||||
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
if endpointKey := ep.Key(); duplicateValidationMap[endpointKey] {
|
||||||
return fmt.Errorf("invalid endpoint %s: %w", endpoint.DisplayName(), err)
|
return fmt.Errorf("invalid endpoint %s: name and group combination must be unique", ep.Key())
|
||||||
|
} else {
|
||||||
|
duplicateValidationMap[endpointKey] = true
|
||||||
|
}
|
||||||
|
if err := ep.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return fmt.Errorf("invalid endpoint %s: %w", ep.Key(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config][validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
||||||
|
// Validate external endpoints
|
||||||
|
for _, ee := range config.ExternalEndpoints {
|
||||||
|
if config.Debug {
|
||||||
|
log.Printf("[config.validateEndpointsConfig] Validating external endpoint '%s'", ee.Name)
|
||||||
|
}
|
||||||
|
if endpointKey := ee.Key(); duplicateValidationMap[endpointKey] {
|
||||||
|
return fmt.Errorf("invalid external endpoint %s: name and group combination must be unique", ee.Key())
|
||||||
|
} else {
|
||||||
|
duplicateValidationMap[endpointKey] = true
|
||||||
|
}
|
||||||
|
if err := ee.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return fmt.Errorf("invalid external endpoint %s: %w", ee.Key(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("[config.validateEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +373,7 @@ func validateSecurityConfig(config *Config) error {
|
|||||||
if config.Security != nil {
|
if config.Security != nil {
|
||||||
if config.Security.IsValid() {
|
if config.Security.IsValid() {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config][validateSecurityConfig] Basic security configuration has been validated")
|
log.Printf("[config.validateSecurityConfig] Basic security configuration has been validated")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If there was an attempt to configure security, then it must mean that some confidential or private
|
// If there was an attempt to configure security, then it must mean that some confidential or private
|
||||||
@@ -353,22 +386,23 @@ func validateSecurityConfig(config *Config) error {
|
|||||||
|
|
||||||
// validateAlertingConfig validates the alerting configuration
|
// validateAlertingConfig validates the alerting configuration
|
||||||
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
|
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
|
||||||
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Endpoint.ValidateAndSetDefaults()
|
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before endpoint.Endpoint.ValidateAndSetDefaults()
|
||||||
// sets the default alert values when none are set.
|
// sets the default alert values when none are set.
|
||||||
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.Endpoint, debug bool) {
|
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoint.Endpoint, externalEndpoints []*endpoint.ExternalEndpoint, debug bool) {
|
||||||
if alertingConfig == nil {
|
if alertingConfig == nil {
|
||||||
log.Printf("[config][validateAlertingConfig] Alerting is not configured")
|
log.Printf("[config.validateAlertingConfig] Alerting is not configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertTypes := []alert.Type{
|
alertTypes := []alert.Type{
|
||||||
alert.TypeAWSSES,
|
alert.TypeAWSSES,
|
||||||
alert.TypeCustom,
|
alert.TypeCustom,
|
||||||
alert.TypeDiscord,
|
alert.TypeDiscord,
|
||||||
|
alert.TypeEmail,
|
||||||
alert.TypeGitHub,
|
alert.TypeGitHub,
|
||||||
alert.TypeGitLab,
|
alert.TypeGitLab,
|
||||||
alert.TypeGoogleChat,
|
alert.TypeGoogleChat,
|
||||||
alert.TypeGotify,
|
alert.TypeGotify,
|
||||||
alert.TypeEmail,
|
alert.TypeJetBrainsSpace,
|
||||||
alert.TypeMatrix,
|
alert.TypeMatrix,
|
||||||
alert.TypeMattermost,
|
alert.TypeMattermost,
|
||||||
alert.TypeMessagebird,
|
alert.TypeMessagebird,
|
||||||
@@ -388,11 +422,21 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
if alertProvider.IsValid() {
|
if alertProvider.IsValid() {
|
||||||
// Parse alerts with the provider's default alert
|
// Parse alerts with the provider's default alert
|
||||||
if alertProvider.GetDefaultAlert() != nil {
|
if alertProvider.GetDefaultAlert() != nil {
|
||||||
for _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
for alertIndex, endpointAlert := range endpoint.Alerts {
|
for alertIndex, endpointAlert := range ep.Alerts {
|
||||||
if alertType == endpointAlert.Type {
|
if alertType == endpointAlert.Type {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, endpoint.Name)
|
log.Printf("[config.validateAlertingConfig] Parsing alert %d with default alert for provider=%s in endpoint with key=%s", alertIndex, alertType, ep.Key())
|
||||||
|
}
|
||||||
|
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ee := range externalEndpoints {
|
||||||
|
for alertIndex, endpointAlert := range ee.Alerts {
|
||||||
|
if alertType == endpointAlert.Type {
|
||||||
|
if debug {
|
||||||
|
log.Printf("[config.validateAlertingConfig] Parsing alert %d with default alert for provider=%s in endpoint with key=%s", alertIndex, alertType, ee.Key())
|
||||||
}
|
}
|
||||||
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
||||||
}
|
}
|
||||||
@@ -401,7 +445,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
}
|
}
|
||||||
validProviders = append(validProviders, alertType)
|
validProviders = append(validProviders, alertType)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[config][validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
|
log.Printf("[config.validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
|
||||||
invalidProviders = append(invalidProviders, alertType)
|
invalidProviders = append(invalidProviders, alertType)
|
||||||
alertingConfig.SetAlertingProviderToNil(alertProvider)
|
alertingConfig.SetAlertingProviderToNil(alertProvider)
|
||||||
}
|
}
|
||||||
@@ -409,5 +453,5 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
invalidProviders = append(invalidProviders, alertType)
|
invalidProviders = append(invalidProviders, alertType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
|
log.Printf("[config.validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
|
||||||
}
|
}
|
||||||
|
|||||||
+362
-57
@@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
||||||
@@ -28,13 +30,14 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadConfiguration(t *testing.T) {
|
func TestLoadConfiguration(t *testing.T) {
|
||||||
|
yes := true
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -64,7 +67,7 @@ func TestLoadConfiguration(t *testing.T) {
|
|||||||
endpoints:
|
endpoints:
|
||||||
- name: website`,
|
- name: website`,
|
||||||
},
|
},
|
||||||
expectedError: core.ErrEndpointWithNoURL,
|
expectedError: endpoint.ErrEndpointWithNoURL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config-file-with-endpoint-that-has-no-conditions",
|
name: "config-file-with-endpoint-that-has-no-conditions",
|
||||||
@@ -75,7 +78,7 @@ endpoints:
|
|||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health`,
|
url: https://twin.sh/health`,
|
||||||
},
|
},
|
||||||
expectedError: core.ErrEndpointWithNoCondition,
|
expectedError: endpoint.ErrEndpointWithNoCondition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config-file",
|
name: "config-file",
|
||||||
@@ -89,11 +92,11 @@ endpoints:
|
|||||||
- "[STATUS] == 200"`,
|
- "[STATUS] == 200"`,
|
||||||
},
|
},
|
||||||
expectedConfig: &Config{
|
expectedConfig: &Config{
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "website",
|
Name: "website",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -135,21 +138,21 @@ endpoints:
|
|||||||
- "[BODY].status == UP"`,
|
- "[BODY].status == UP"`,
|
||||||
},
|
},
|
||||||
expectedConfig: &Config{
|
expectedConfig: &Config{
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "one",
|
Name: "one",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Conditions: []core.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "two",
|
Name: "two",
|
||||||
URL: "https://example.org",
|
URL: "https://example.org",
|
||||||
Conditions: []core.Condition{"len([BODY]) > 0"},
|
Conditions: []endpoint.Condition{"len([BODY]) > 0"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "three",
|
Name: "three",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -164,6 +167,8 @@ metrics: true
|
|||||||
alerting:
|
alerting:
|
||||||
slack:
|
slack:
|
||||||
webhook-url: https://hooks.slack.com/services/xxx/yyy/zzz
|
webhook-url: https://hooks.slack.com/services/xxx/yyy/zzz
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: example
|
- name: example
|
||||||
@@ -178,6 +183,12 @@ alerting:
|
|||||||
discord:
|
discord:
|
||||||
webhook-url: https://discord.com/api/webhooks/xxx/yyy
|
webhook-url: https://discord.com/api/webhooks/xxx/yyy
|
||||||
|
|
||||||
|
external-endpoints:
|
||||||
|
- name: ext-ep-test
|
||||||
|
token: "potato"
|
||||||
|
alerts:
|
||||||
|
- type: slack
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: frontend
|
- name: frontend
|
||||||
url: https://example.com
|
url: https://example.com
|
||||||
@@ -189,19 +200,32 @@ endpoints:
|
|||||||
Metrics: true,
|
Metrics: true,
|
||||||
Alerting: &alerting.Config{
|
Alerting: &alerting.Config{
|
||||||
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
|
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
|
||||||
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
|
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz", DefaultAlert: &alert.Alert{Enabled: &yes}},
|
||||||
},
|
},
|
||||||
Endpoints: []*core.Endpoint{
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
||||||
|
{
|
||||||
|
Name: "ext-ep-test",
|
||||||
|
Token: "potato",
|
||||||
|
Alerts: []*alert.Alert{
|
||||||
|
{
|
||||||
|
Type: alert.TypeSlack,
|
||||||
|
FailureThreshold: 3,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "https://example.org",
|
URL: "https://example.org",
|
||||||
Interval: 5 * time.Second,
|
Interval: 5 * time.Second,
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -303,11 +327,13 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
|
|||||||
storage:
|
storage:
|
||||||
type: sqlite
|
type: sqlite
|
||||||
path: %s
|
path: %s
|
||||||
|
|
||||||
maintenance:
|
maintenance:
|
||||||
enabled: true
|
enabled: true
|
||||||
start: 00:00
|
start: 00:00
|
||||||
duration: 4h
|
duration: 4h
|
||||||
every: [Monday, Thursday]
|
every: [Monday, Thursday]
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
title: T
|
title: T
|
||||||
header: H
|
header: H
|
||||||
@@ -317,6 +343,12 @@ ui:
|
|||||||
link: "https://example.org"
|
link: "https://example.org"
|
||||||
- name: "Status page"
|
- name: "Status page"
|
||||||
link: "https://status.example.org"
|
link: "https://status.example.org"
|
||||||
|
|
||||||
|
external-endpoints:
|
||||||
|
- name: ext-ep-test
|
||||||
|
group: core
|
||||||
|
token: "potato"
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health
|
url: https://twin.sh/health
|
||||||
@@ -357,10 +389,22 @@ endpoints:
|
|||||||
if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 {
|
if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 {
|
||||||
t.Error("Expected Config.Maintenance to be configured properly")
|
t.Error("Expected Config.Maintenance to be configured properly")
|
||||||
}
|
}
|
||||||
|
if len(config.ExternalEndpoints) != 1 {
|
||||||
|
t.Error("Should have returned one external endpoint")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Name != "ext-ep-test" {
|
||||||
|
t.Errorf("Name should have been %s", "ext-ep-test")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Group != "core" {
|
||||||
|
t.Errorf("Group should have been %s", "core")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Token != "potato" {
|
||||||
|
t.Errorf("Token should have been %s", "potato")
|
||||||
|
}
|
||||||
|
|
||||||
if len(config.Endpoints) != 3 {
|
if len(config.Endpoints) != 3 {
|
||||||
t.Error("Should have returned two endpoints")
|
t.Error("Should have returned two endpoints")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
||||||
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
||||||
}
|
}
|
||||||
@@ -382,7 +426,6 @@ endpoints:
|
|||||||
if len(config.Endpoints[0].Conditions) != 1 {
|
if len(config.Endpoints[0].Conditions) != 1 {
|
||||||
t.Errorf("There should have been %d conditions", 1)
|
t.Errorf("There should have been %d conditions", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[1].URL != "https://api.github.com/healthz" {
|
if config.Endpoints[1].URL != "https://api.github.com/healthz" {
|
||||||
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
|
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
|
||||||
}
|
}
|
||||||
@@ -404,7 +447,6 @@ endpoints:
|
|||||||
if len(config.Endpoints[1].Conditions) != 2 {
|
if len(config.Endpoints[1].Conditions) != 2 {
|
||||||
t.Errorf("There should have been %d conditions", 2)
|
t.Errorf("There should have been %d conditions", 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[2].URL != "https://example.com/" {
|
if config.Endpoints[2].URL != "https://example.com/" {
|
||||||
t.Errorf("URL should have been %s", "https://example.com/")
|
t.Errorf("URL should have been %s", "https://example.com/")
|
||||||
}
|
}
|
||||||
@@ -654,8 +696,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != core.GatusUserAgent {
|
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != endpoint.GatusUserAgent {
|
||||||
t.Errorf("User-Agent should've been %s because it's the default value, got %s", core.GatusUserAgent, userAgent)
|
t.Errorf("User-Agent should've been %s because it's the default value, got %s", endpoint.GatusUserAgent, userAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +748,10 @@ alerting:
|
|||||||
to: "+1-234-567-8901"
|
to: "+1-234-567-8901"
|
||||||
teams:
|
teams:
|
||||||
webhook-url: "http://example.com"
|
webhook-url: "http://example.com"
|
||||||
|
jetbrainsspace:
|
||||||
|
project: "foo"
|
||||||
|
channel-id: "bar"
|
||||||
|
token: "baz"
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@@ -728,6 +774,7 @@ endpoints:
|
|||||||
success-threshold: 15
|
success-threshold: 15
|
||||||
- type: teams
|
- type: teams
|
||||||
- type: pushover
|
- type: pushover
|
||||||
|
- type: jetbrainsspace
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
@@ -754,8 +801,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if len(config.Endpoints[0].Alerts) != 9 {
|
if len(config.Endpoints[0].Alerts) != 10 {
|
||||||
t.Fatal("There should've been 9 alerts configured")
|
t.Fatal("There should've been 10 alerts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
@@ -862,6 +909,12 @@ endpoints:
|
|||||||
if !config.Endpoints[0].Alerts[8].IsEnabled() {
|
if !config.Endpoints[0].Alerts[8].IsEnabled() {
|
||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
}
|
}
|
||||||
|
if config.Endpoints[0].Alerts[9].Type != alert.TypeJetBrainsSpace {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeJetBrainsSpace, config.Endpoints[0].Alerts[9].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[9].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
|
||||||
@@ -878,7 +931,7 @@ alerting:
|
|||||||
default-alert:
|
default-alert:
|
||||||
enabled: true
|
enabled: true
|
||||||
failure-threshold: 10
|
failure-threshold: 10
|
||||||
success-threshold: 1
|
success-threshold: 15
|
||||||
pagerduty:
|
pagerduty:
|
||||||
integration-key: "00000000000000000000000000000000"
|
integration-key: "00000000000000000000000000000000"
|
||||||
default-alert:
|
default-alert:
|
||||||
@@ -923,23 +976,57 @@ alerting:
|
|||||||
webhook-url: "http://example.com"
|
webhook-url: "http://example.com"
|
||||||
default-alert:
|
default-alert:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
jetbrainsspace:
|
||||||
|
project: "foo"
|
||||||
|
channel-id: "bar"
|
||||||
|
token: "baz"
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
failure-threshold: 5
|
||||||
|
success-threshold: 3
|
||||||
|
email:
|
||||||
|
from: "from@example.com"
|
||||||
|
username: "from@example.com"
|
||||||
|
password: "hunter2"
|
||||||
|
host: "mail.example.com"
|
||||||
|
port: 587
|
||||||
|
to: "recipient1@example.com,recipient2@example.com"
|
||||||
|
client:
|
||||||
|
insecure: false
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
gotify:
|
||||||
|
server-url: "https://gotify.example"
|
||||||
|
token: "**************"
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
external-endpoints:
|
||||||
|
- name: ext-ep-test
|
||||||
|
group: core
|
||||||
|
token: potato
|
||||||
|
alerts:
|
||||||
|
- type: discord
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health
|
url: https://twin.sh/health
|
||||||
alerts:
|
alerts:
|
||||||
- type: slack
|
- type: slack
|
||||||
- type: pagerduty
|
- type: pagerduty
|
||||||
- type: mattermost
|
- type: mattermost
|
||||||
- type: messagebird
|
- type: messagebird
|
||||||
- type: discord
|
- type: discord
|
||||||
success-threshold: 2 # test endpoint alert override
|
success-threshold: 8 # test endpoint alert override
|
||||||
- type: telegram
|
- type: telegram
|
||||||
- type: twilio
|
- type: twilio
|
||||||
- type: teams
|
- type: teams
|
||||||
- type: pushover
|
- type: pushover
|
||||||
conditions:
|
- type: jetbrainsspace
|
||||||
- "[STATUS] == 200"
|
- type: email
|
||||||
|
- type: gotify
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("expected no error, got", err.Error())
|
t.Error("expected no error, got", err.Error())
|
||||||
@@ -1016,6 +1103,12 @@ endpoints:
|
|||||||
if config.Alerting.Discord.GetDefaultAlert() == nil {
|
if config.Alerting.Discord.GetDefaultAlert() == nil {
|
||||||
t.Fatal("Discord.GetDefaultAlert() shouldn't have returned nil")
|
t.Fatal("Discord.GetDefaultAlert() shouldn't have returned nil")
|
||||||
}
|
}
|
||||||
|
if config.Alerting.Discord.GetDefaultAlert().FailureThreshold != 10 {
|
||||||
|
t.Errorf("Discord default alert failure threshold should've been %d, but was %d", 10, config.Alerting.Discord.GetDefaultAlert().FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Alerting.Discord.GetDefaultAlert().SuccessThreshold != 15 {
|
||||||
|
t.Errorf("Discord default alert success threshold should've been %d, but was %d", 15, config.Alerting.Discord.GetDefaultAlert().SuccessThreshold)
|
||||||
|
}
|
||||||
if config.Alerting.Discord.WebhookURL != "http://example.org" {
|
if config.Alerting.Discord.WebhookURL != "http://example.org" {
|
||||||
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
|
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
|
||||||
}
|
}
|
||||||
@@ -1049,6 +1142,83 @@ endpoints:
|
|||||||
if config.Alerting.Teams.GetDefaultAlert() == nil {
|
if config.Alerting.Teams.GetDefaultAlert() == nil {
|
||||||
t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil")
|
t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil")
|
||||||
}
|
}
|
||||||
|
if config.Alerting.JetBrainsSpace == nil || !config.Alerting.JetBrainsSpace.IsValid() {
|
||||||
|
t.Fatal("JetBrainsSpace alerting config should've been valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Alerting.JetBrainsSpace.GetDefaultAlert() == nil {
|
||||||
|
t.Fatal("JetBrainsSpace.GetDefaultAlert() shouldn't have returned nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.JetBrainsSpace.Project != "foo" {
|
||||||
|
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "foo", config.Alerting.JetBrainsSpace.Project)
|
||||||
|
}
|
||||||
|
if config.Alerting.JetBrainsSpace.ChannelID != "bar" {
|
||||||
|
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "bar", config.Alerting.JetBrainsSpace.ChannelID)
|
||||||
|
}
|
||||||
|
if config.Alerting.JetBrainsSpace.Token != "baz" {
|
||||||
|
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "baz", config.Alerting.JetBrainsSpace.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Alerting.Email == nil || !config.Alerting.Email.IsValid() {
|
||||||
|
t.Fatal("Email alerting config should've been valid")
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.GetDefaultAlert() == nil {
|
||||||
|
t.Fatal("Email.GetDefaultAlert() shouldn't have returned nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.From != "from@example.com" {
|
||||||
|
t.Errorf("Email from should've been %s, but was %s", "from@example.com", config.Alerting.Email.From)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Username != "from@example.com" {
|
||||||
|
t.Errorf("Email username should've been %s, but was %s", "from@example.com", config.Alerting.Email.Username)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Password != "hunter2" {
|
||||||
|
t.Errorf("Email password should've been %s, but was %s", "hunter2", config.Alerting.Email.Password)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Host != "mail.example.com" {
|
||||||
|
t.Errorf("Email host should've been %s, but was %s", "mail.example.com", config.Alerting.Email.Host)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Port != 587 {
|
||||||
|
t.Errorf("Email port should've been %d, but was %d", 587, config.Alerting.Email.Port)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.To != "recipient1@example.com,recipient2@example.com" {
|
||||||
|
t.Errorf("Email to should've been %s, but was %s", "recipient1@example.com,recipient2@example.com", config.Alerting.Email.To)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.ClientConfig == nil {
|
||||||
|
t.Fatal("Email client config should've been set")
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.ClientConfig.Insecure {
|
||||||
|
t.Error("Email client config should've been secure")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Alerting.Gotify == nil || !config.Alerting.Gotify.IsValid() {
|
||||||
|
t.Fatal("Gotify alerting config should've been valid")
|
||||||
|
}
|
||||||
|
if config.Alerting.Gotify.GetDefaultAlert() == nil {
|
||||||
|
t.Fatal("Gotify.GetDefaultAlert() shouldn't have returned nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.Gotify.ServerURL != "https://gotify.example" {
|
||||||
|
t.Errorf("Gotify server URL should've been %s, but was %s", "https://gotify.example", config.Alerting.Gotify.ServerURL)
|
||||||
|
}
|
||||||
|
if config.Alerting.Gotify.Token != "**************" {
|
||||||
|
t.Errorf("Gotify token should've been %s, but was %s", "**************", config.Alerting.Gotify.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// External endpoints
|
||||||
|
if len(config.ExternalEndpoints) != 1 {
|
||||||
|
t.Error("There should've been 1 external endpoint")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Alerts[0].Type != alert.TypeDiscord {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.ExternalEndpoints[0].Alerts[0].Type)
|
||||||
|
}
|
||||||
|
if !config.ExternalEndpoints[0].Alerts[0].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Alerts[0].FailureThreshold != 10 {
|
||||||
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.ExternalEndpoints[0].Alerts[0].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Alerts[0].SuccessThreshold != 15 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.ExternalEndpoints[0].Alerts[0].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
// Endpoints
|
// Endpoints
|
||||||
if len(config.Endpoints) != 1 {
|
if len(config.Endpoints) != 1 {
|
||||||
@@ -1060,8 +1230,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if len(config.Endpoints[0].Alerts) != 9 {
|
if len(config.Endpoints[0].Alerts) != 12 {
|
||||||
t.Fatal("There should've been 9 alerts configured")
|
t.Fatalf("There should've been 12 alerts configured, got %d", len(config.Endpoints[0].Alerts))
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
@@ -1122,8 +1292,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Alerts[4].FailureThreshold != 10 {
|
if config.Endpoints[0].Alerts[4].FailureThreshold != 10 {
|
||||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[4].FailureThreshold)
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[4].FailureThreshold)
|
||||||
}
|
}
|
||||||
if config.Endpoints[0].Alerts[4].SuccessThreshold != 2 {
|
if config.Endpoints[0].Alerts[4].SuccessThreshold != 8 {
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[4].SuccessThreshold)
|
t.Errorf("The default success threshold of the alert should've been %d because it was explicitly overriden, but it was %d", 8, config.Endpoints[0].Alerts[4].SuccessThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[5].Type != alert.TypeTelegram {
|
if config.Endpoints[0].Alerts[5].Type != alert.TypeTelegram {
|
||||||
@@ -1178,6 +1348,44 @@ endpoints:
|
|||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold)
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[8].SuccessThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[9].Type != alert.TypeJetBrainsSpace {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeJetBrainsSpace, config.Endpoints[0].Alerts[9].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[9].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[9].FailureThreshold != 5 {
|
||||||
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 5, config.Endpoints[0].Alerts[9].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[9].SuccessThreshold != 3 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[10].Type != alert.TypeEmail {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeEmail, config.Endpoints[0].Alerts[10].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[10].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[10].FailureThreshold != 3 {
|
||||||
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[10].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[10].SuccessThreshold != 2 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[10].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[11].Type != alert.TypeGotify {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeGotify, config.Endpoints[0].Alerts[11].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[11].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[11].FailureThreshold != 3 {
|
||||||
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[11].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[11].SuccessThreshold != 2 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[11].SuccessThreshold)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
|
||||||
@@ -1450,6 +1658,99 @@ endpoints:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAndValidateConfigBytesWithDuplicateEndpointName(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
shouldError bool
|
||||||
|
config string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "same-name-no-group",
|
||||||
|
shouldError: true,
|
||||||
|
config: `
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-different-group",
|
||||||
|
shouldError: false,
|
||||||
|
config: `
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- name: ep1
|
||||||
|
group: g1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-same-group",
|
||||||
|
shouldError: true,
|
||||||
|
config: `
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
group: g1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- name: ep1
|
||||||
|
group: g1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-different-endpoint-type",
|
||||||
|
shouldError: true,
|
||||||
|
config: `
|
||||||
|
external-endpoints:
|
||||||
|
- name: ep1
|
||||||
|
token: "12345678"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-different-group-different-endpoint-type",
|
||||||
|
shouldError: false,
|
||||||
|
config: `
|
||||||
|
external-endpoints:
|
||||||
|
- name: ep1
|
||||||
|
group: gr1
|
||||||
|
token: "12345678"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
||||||
|
if scenario.shouldError && err == nil {
|
||||||
|
t.Error("should've returned an error")
|
||||||
|
} else if !scenario.shouldError && err != nil {
|
||||||
|
t.Error("shouldn't have returned an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithInvalidStorageConfig(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithInvalidStorageConfig(t *testing.T) {
|
||||||
_, err := parseAndValidateConfigBytes([]byte(`
|
_, err := parseAndValidateConfigBytes([]byte(`
|
||||||
storage:
|
storage:
|
||||||
@@ -1563,29 +1864,31 @@ endpoints:
|
|||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithNoEndpoints(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithNoEndpoints(t *testing.T) {
|
||||||
_, err := parseAndValidateConfigBytes([]byte(``))
|
_, err := parseAndValidateConfigBytes([]byte(``))
|
||||||
if err != ErrNoEndpointInConfig {
|
if !errors.Is(err, ErrNoEndpointInConfig) {
|
||||||
t.Error("The error returned should have been of type ErrNoEndpointInConfig")
|
t.Error("The error returned should have been of type ErrNoEndpointInConfig")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAlertingProviderByAlertType(t *testing.T) {
|
func TestGetAlertingProviderByAlertType(t *testing.T) {
|
||||||
alertingConfig := &alerting.Config{
|
alertingConfig := &alerting.Config{
|
||||||
Custom: &custom.AlertProvider{},
|
Custom: &custom.AlertProvider{},
|
||||||
Discord: &discord.AlertProvider{},
|
Discord: &discord.AlertProvider{},
|
||||||
Email: &email.AlertProvider{},
|
Email: &email.AlertProvider{},
|
||||||
GitHub: &github.AlertProvider{},
|
GitHub: &github.AlertProvider{},
|
||||||
GoogleChat: &googlechat.AlertProvider{},
|
GoogleChat: &googlechat.AlertProvider{},
|
||||||
Matrix: &matrix.AlertProvider{},
|
Gotify: &gotify.AlertProvider{},
|
||||||
Mattermost: &mattermost.AlertProvider{},
|
JetBrainsSpace: &jetbrainsspace.AlertProvider{},
|
||||||
Messagebird: &messagebird.AlertProvider{},
|
Matrix: &matrix.AlertProvider{},
|
||||||
Ntfy: &ntfy.AlertProvider{},
|
Mattermost: &mattermost.AlertProvider{},
|
||||||
Opsgenie: &opsgenie.AlertProvider{},
|
Messagebird: &messagebird.AlertProvider{},
|
||||||
PagerDuty: &pagerduty.AlertProvider{},
|
Ntfy: &ntfy.AlertProvider{},
|
||||||
Pushover: &pushover.AlertProvider{},
|
Opsgenie: &opsgenie.AlertProvider{},
|
||||||
Slack: &slack.AlertProvider{},
|
PagerDuty: &pagerduty.AlertProvider{},
|
||||||
Telegram: &telegram.AlertProvider{},
|
Pushover: &pushover.AlertProvider{},
|
||||||
Twilio: &twilio.AlertProvider{},
|
Slack: &slack.AlertProvider{},
|
||||||
Teams: &teams.AlertProvider{},
|
Telegram: &telegram.AlertProvider{},
|
||||||
|
Twilio: &twilio.AlertProvider{},
|
||||||
|
Teams: &teams.AlertProvider{},
|
||||||
}
|
}
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
alertType alert.Type
|
alertType alert.Type
|
||||||
@@ -1596,6 +1899,8 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
|
|||||||
{alertType: alert.TypeEmail, expected: alertingConfig.Email},
|
{alertType: alert.TypeEmail, expected: alertingConfig.Email},
|
||||||
{alertType: alert.TypeGitHub, expected: alertingConfig.GitHub},
|
{alertType: alert.TypeGitHub, expected: alertingConfig.GitHub},
|
||||||
{alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat},
|
{alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat},
|
||||||
|
{alertType: alert.TypeGotify, expected: alertingConfig.Gotify},
|
||||||
|
{alertType: alert.TypeJetBrainsSpace, expected: alertingConfig.JetBrainsSpace},
|
||||||
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
|
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
|
||||||
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
|
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
|
||||||
{alertType: alert.TypeMessagebird, expected: alertingConfig.Messagebird},
|
{alertType: alert.TypeMessagebird, expected: alertingConfig.Messagebird},
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type Checker struct {
|
|||||||
lastCheck time.Time
|
lastCheck time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Checker) Check() bool {
|
func (c *Checker) Check() bool {
|
||||||
return client.CanCreateTCPConnection(c.Target, &client.Config{Timeout: 5 * time.Second})
|
return client.CanCreateTCPConnection(c.Target, &client.Config{Timeout: 5 * time.Second})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrEndpointWithNoName is the error with which Gatus will panic if an endpoint is configured with no name
|
||||||
|
ErrEndpointWithNoName = errors.New("you must specify a name for each endpoint")
|
||||||
|
|
||||||
|
// ErrEndpointWithInvalidNameOrGroup is the error with which Gatus will panic if an endpoint has an invalid character where it shouldn't
|
||||||
|
ErrEndpointWithInvalidNameOrGroup = errors.New("endpoint name and group must not have \" or \\")
|
||||||
|
)
|
||||||
|
|
||||||
|
// validateEndpointNameGroupAndAlerts validates the name, group and alerts of an endpoint
|
||||||
|
func validateEndpointNameGroupAndAlerts(name, group string, alerts []*alert.Alert) error {
|
||||||
|
if len(name) == 0 {
|
||||||
|
return ErrEndpointWithNoName
|
||||||
|
}
|
||||||
|
if strings.ContainsAny(name, "\"\\") || strings.ContainsAny(group, "\"\\") {
|
||||||
|
return ErrEndpointWithInvalidNameOrGroup
|
||||||
|
}
|
||||||
|
for _, endpointAlert := range alerts {
|
||||||
|
if err := endpointAlert.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateEndpointNameGroupAndAlerts(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
group string
|
||||||
|
alerts []*alert.Alert
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "n",
|
||||||
|
group: "g",
|
||||||
|
alerts: []*alert.Alert{{Type: "slack"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "n",
|
||||||
|
alerts: []*alert.Alert{{Type: "slack"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "g",
|
||||||
|
alerts: []*alert.Alert{{Type: "slack"}},
|
||||||
|
expectedErr: ErrEndpointWithNoName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "\"",
|
||||||
|
alerts: []*alert.Alert{{Type: "slack"}},
|
||||||
|
expectedErr: ErrEndpointWithInvalidNameOrGroup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "n",
|
||||||
|
group: "\\",
|
||||||
|
alerts: []*alert.Alert{{Type: "slack"}},
|
||||||
|
expectedErr: ErrEndpointWithInvalidNameOrGroup,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
err := validateEndpointNameGroupAndAlerts(scenario.name, scenario.group, scenario.alerts)
|
||||||
|
if !errors.Is(err, scenario.expectedErr) {
|
||||||
|
t.Errorf("expected error to be %v but got %v", scenario.expectedErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -150,7 +150,7 @@ func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool) bo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !success {
|
if !success {
|
||||||
//log.Printf("[Condition][evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
|
//log.Printf("[Condition.evaluate] Condition '%s' did not succeed because '%s' is false", condition, condition)
|
||||||
}
|
}
|
||||||
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
|
result.ConditionResults = append(result.ConditionResults, &ConditionResult{Condition: conditionToDisplay, Success: success})
|
||||||
return success
|
return success
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
// ConditionResult result of a Condition
|
// ConditionResult result of a Condition
|
||||||
type ConditionResult struct {
|
type ConditionResult struct {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -259,6 +259,27 @@ func TestCondition_evaluate(t *testing.T) {
|
|||||||
ExpectedSuccess: true,
|
ExpectedSuccess: true,
|
||||||
ExpectedOutput: "[BODY][0].id == 1",
|
ExpectedOutput: "[BODY][0].id == 1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "body-jsonpath-when-body-has-null-parameter",
|
||||||
|
Condition: Condition("[BODY].data == OK"),
|
||||||
|
Result: &Result{Body: []byte(`{"data": null}"`)},
|
||||||
|
ExpectedSuccess: false,
|
||||||
|
ExpectedOutput: "[BODY].data (INVALID) == OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "body-jsonpath-when-body-has-array-with-null",
|
||||||
|
Condition: Condition("[BODY].items[0] == OK"),
|
||||||
|
Result: &Result{Body: []byte(`{"items": [null, null]}"`)},
|
||||||
|
ExpectedSuccess: false,
|
||||||
|
ExpectedOutput: "[BODY].items[0] (INVALID) == OK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "body-jsonpath-when-body-is-null",
|
||||||
|
Condition: Condition("[BODY].data == OK"),
|
||||||
|
Result: &Result{Body: []byte(`null`)},
|
||||||
|
ExpectedSuccess: false,
|
||||||
|
ExpectedOutput: "[BODY].data (INVALID) == OK",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "body-jsonpath-when-body-is-array-but-actual-body-is-not",
|
Name: "body-jsonpath-when-body-is-array-but-actual-body-is-not",
|
||||||
Condition: Condition("[BODY][0].name == test"),
|
Condition: Condition("[BODY][0].name == test"),
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDNSWithNoQueryName is the error with which gatus will panic if a dns is configured without query name
|
||||||
|
ErrDNSWithNoQueryName = errors.New("you must specify a query name in the DNS configuration")
|
||||||
|
|
||||||
|
// ErrDNSWithInvalidQueryType is the error with which gatus will panic if a dns is configured with invalid query type
|
||||||
|
ErrDNSWithInvalidQueryType = errors.New("invalid query type in the DNS configuration")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config for an Endpoint of type DNS
|
||||||
|
type Config struct {
|
||||||
|
// QueryType is the type for the DNS records like A, AAAA, CNAME...
|
||||||
|
QueryType string `yaml:"query-type"`
|
||||||
|
|
||||||
|
// QueryName is the query for DNS
|
||||||
|
QueryName string `yaml:"query-name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Config) ValidateAndSetDefault() error {
|
||||||
|
if len(d.QueryName) == 0 {
|
||||||
|
return ErrDNSWithNoQueryName
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(d.QueryName, ".") {
|
||||||
|
d.QueryName += "."
|
||||||
|
}
|
||||||
|
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
||||||
|
return ErrDNSWithInvalidQueryType
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_ValidateAndSetDefault(t *testing.T) {
|
||||||
|
dns := &Config{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "",
|
||||||
|
}
|
||||||
|
err := dns.ValidateAndSetDefault()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error because endpoint's dns didn't have a query name, which is a mandatory field for dns")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||||
|
dns := &Config{
|
||||||
|
QueryType: "B",
|
||||||
|
QueryName: "example.com",
|
||||||
|
}
|
||||||
|
err := dns.ValidateAndSetDefault()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error because endpoint's dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -15,12 +15,13 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EndpointType string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HostHeader is the name of the header used to specify the host
|
// HostHeader is the name of the header used to specify the host
|
||||||
@@ -35,17 +36,17 @@ const (
|
|||||||
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
||||||
GatusUserAgent = "Gatus/1.0"
|
GatusUserAgent = "Gatus/1.0"
|
||||||
|
|
||||||
EndpointTypeDNS EndpointType = "DNS"
|
TypeDNS Type = "DNS"
|
||||||
EndpointTypeTCP EndpointType = "TCP"
|
TypeTCP Type = "TCP"
|
||||||
EndpointTypeSCTP EndpointType = "SCTP"
|
TypeSCTP Type = "SCTP"
|
||||||
EndpointTypeUDP EndpointType = "UDP"
|
TypeUDP Type = "UDP"
|
||||||
EndpointTypeICMP EndpointType = "ICMP"
|
TypeICMP Type = "ICMP"
|
||||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
TypeSTARTTLS Type = "STARTTLS"
|
||||||
EndpointTypeTLS EndpointType = "TLS"
|
TypeTLS Type = "TLS"
|
||||||
EndpointTypeHTTP EndpointType = "HTTP"
|
TypeHTTP Type = "HTTP"
|
||||||
EndpointTypeWS EndpointType = "WEBSOCKET"
|
TypeWS Type = "WEBSOCKET"
|
||||||
EndpointTypeSSH EndpointType = "SSH"
|
TypeSSH Type = "SSH"
|
||||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
TypeUNKNOWN Type = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -55,12 +56,6 @@ var (
|
|||||||
// ErrEndpointWithNoURL is the error with which Gatus will panic if an endpoint is configured with no url
|
// ErrEndpointWithNoURL is the error with which Gatus will panic if an endpoint is configured with no url
|
||||||
ErrEndpointWithNoURL = errors.New("you must specify an url for each endpoint")
|
ErrEndpointWithNoURL = errors.New("you must specify an url for each endpoint")
|
||||||
|
|
||||||
// ErrEndpointWithNoName is the error with which Gatus will panic if an endpoint is configured with no name
|
|
||||||
ErrEndpointWithNoName = errors.New("you must specify a name for each endpoint")
|
|
||||||
|
|
||||||
// ErrEndpointWithInvalidNameOrGroup is the error with which Gatus will panic if an endpoint has an invalid character where it shouldn't
|
|
||||||
ErrEndpointWithInvalidNameOrGroup = errors.New("endpoint name and group must not have \" or \\")
|
|
||||||
|
|
||||||
// ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type
|
// ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type
|
||||||
ErrUnknownEndpointType = errors.New("unknown endpoint type")
|
ErrUnknownEndpointType = errors.New("unknown endpoint type")
|
||||||
|
|
||||||
@@ -72,13 +67,9 @@ var (
|
|||||||
// This is because the free whois service we are using should not be abused, especially considering the fact that
|
// 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.
|
// 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)")
|
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
|
// Endpoint is the configuration of a service to be monitored
|
||||||
type Endpoint struct {
|
type Endpoint struct {
|
||||||
// Enabled defines whether to enable the monitoring of the endpoint
|
// Enabled defines whether to enable the monitoring of the endpoint
|
||||||
Enabled *bool `yaml:"enabled,omitempty"`
|
Enabled *bool `yaml:"enabled,omitempty"`
|
||||||
@@ -92,9 +83,6 @@ type Endpoint struct {
|
|||||||
// URL to send the request to
|
// URL to send the request to
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
|
|
||||||
// DNS is the configuration of DNS monitoring
|
|
||||||
DNS *DNS `yaml:"dns,omitempty"`
|
|
||||||
|
|
||||||
// Method of the request made to the url of the endpoint
|
// Method of the request made to the url of the endpoint
|
||||||
Method string `yaml:"method,omitempty"`
|
Method string `yaml:"method,omitempty"`
|
||||||
|
|
||||||
@@ -116,6 +104,12 @@ type Endpoint struct {
|
|||||||
// Alerts is the alerting configuration for the endpoint in case of failure
|
// Alerts is the alerting configuration for the endpoint in case of failure
|
||||||
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
||||||
|
|
||||||
|
// DNSConfig is the configuration for DNS monitoring
|
||||||
|
DNSConfig *dns.Config `yaml:"dns,omitempty"`
|
||||||
|
|
||||||
|
// SSH is the configuration for SSH monitoring
|
||||||
|
SSHConfig *sshconfig.Config `yaml:"ssh,omitempty"`
|
||||||
|
|
||||||
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
|
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
|
||||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
|
||||||
@@ -127,163 +121,142 @@ type Endpoint struct {
|
|||||||
|
|
||||||
// NumberOfSuccessesInARow is the number of successful evaluations in a row
|
// NumberOfSuccessesInARow is the number of successful evaluations in a row
|
||||||
NumberOfSuccessesInARow int `yaml:"-"`
|
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
|
// IsEnabled returns whether the endpoint is enabled or not
|
||||||
func (endpoint Endpoint) IsEnabled() bool {
|
func (e *Endpoint) IsEnabled() bool {
|
||||||
if endpoint.Enabled == nil {
|
if e.Enabled == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *endpoint.Enabled
|
return *e.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns the endpoint type
|
// Type returns the endpoint type
|
||||||
func (endpoint Endpoint) Type() EndpointType {
|
func (e *Endpoint) Type() Type {
|
||||||
switch {
|
switch {
|
||||||
case endpoint.DNS != nil:
|
case e.DNSConfig != nil:
|
||||||
return EndpointTypeDNS
|
return TypeDNS
|
||||||
case strings.HasPrefix(endpoint.URL, "tcp://"):
|
case strings.HasPrefix(e.URL, "tcp://"):
|
||||||
return EndpointTypeTCP
|
return TypeTCP
|
||||||
case strings.HasPrefix(endpoint.URL, "sctp://"):
|
case strings.HasPrefix(e.URL, "sctp://"):
|
||||||
return EndpointTypeSCTP
|
return TypeSCTP
|
||||||
case strings.HasPrefix(endpoint.URL, "udp://"):
|
case strings.HasPrefix(e.URL, "udp://"):
|
||||||
return EndpointTypeUDP
|
return TypeUDP
|
||||||
case strings.HasPrefix(endpoint.URL, "icmp://"):
|
case strings.HasPrefix(e.URL, "icmp://"):
|
||||||
return EndpointTypeICMP
|
return TypeICMP
|
||||||
case strings.HasPrefix(endpoint.URL, "starttls://"):
|
case strings.HasPrefix(e.URL, "starttls://"):
|
||||||
return EndpointTypeSTARTTLS
|
return TypeSTARTTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "tls://"):
|
case strings.HasPrefix(e.URL, "tls://"):
|
||||||
return EndpointTypeTLS
|
return TypeTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
|
||||||
return EndpointTypeHTTP
|
return TypeHTTP
|
||||||
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
|
||||||
return EndpointTypeWS
|
return TypeWS
|
||||||
case strings.HasPrefix(endpoint.URL, "ssh://"):
|
case strings.HasPrefix(e.URL, "ssh://"):
|
||||||
return EndpointTypeSSH
|
return TypeSSH
|
||||||
default:
|
default:
|
||||||
return EndpointTypeUNKNOWN
|
return TypeUNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
||||||
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
func (e *Endpoint) ValidateAndSetDefaults() error {
|
||||||
// Set default values
|
if err := validateEndpointNameGroupAndAlerts(e.Name, e.Group, e.Alerts); err != nil {
|
||||||
if endpoint.ClientConfig == nil {
|
return err
|
||||||
endpoint.ClientConfig = client.GetDefaultConfig()
|
}
|
||||||
|
if len(e.URL) == 0 {
|
||||||
|
return ErrEndpointWithNoURL
|
||||||
|
}
|
||||||
|
if e.ClientConfig == nil {
|
||||||
|
e.ClientConfig = client.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
if err := e.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig == nil {
|
if e.UIConfig == nil {
|
||||||
endpoint.UIConfig = ui.GetDefaultConfig()
|
e.UIConfig = ui.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
if err := endpoint.UIConfig.ValidateAndSetDefaults(); err != nil {
|
if err := e.UIConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.Interval == 0 {
|
if e.Interval == 0 {
|
||||||
endpoint.Interval = 1 * time.Minute
|
e.Interval = 1 * time.Minute
|
||||||
}
|
}
|
||||||
if len(endpoint.Method) == 0 {
|
if len(e.Method) == 0 {
|
||||||
endpoint.Method = http.MethodGet
|
e.Method = http.MethodGet
|
||||||
}
|
}
|
||||||
if len(endpoint.Headers) == 0 {
|
if len(e.Headers) == 0 {
|
||||||
endpoint.Headers = make(map[string]string)
|
e.Headers = make(map[string]string)
|
||||||
}
|
}
|
||||||
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
||||||
if _, userAgentHeaderExists := endpoint.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
if _, userAgentHeaderExists := e.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
||||||
endpoint.Headers[UserAgentHeader] = GatusUserAgent
|
e.Headers[UserAgentHeader] = GatusUserAgent
|
||||||
}
|
}
|
||||||
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
||||||
// and endpoint.GraphQL is set to true
|
// and endpoint.GraphQL is set to true
|
||||||
if _, contentTypeHeaderExists := endpoint.Headers[ContentTypeHeader]; !contentTypeHeaderExists && endpoint.GraphQL {
|
if _, contentTypeHeaderExists := e.Headers[ContentTypeHeader]; !contentTypeHeaderExists && e.GraphQL {
|
||||||
endpoint.Headers[ContentTypeHeader] = "application/json"
|
e.Headers[ContentTypeHeader] = "application/json"
|
||||||
}
|
}
|
||||||
for _, endpointAlert := range endpoint.Alerts {
|
if len(e.Conditions) == 0 {
|
||||||
if err := endpointAlert.ValidateAndSetDefaults(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(endpoint.Name) == 0 {
|
|
||||||
return ErrEndpointWithNoName
|
|
||||||
}
|
|
||||||
if strings.ContainsAny(endpoint.Name, "\"\\") || strings.ContainsAny(endpoint.Group, "\"\\") {
|
|
||||||
return ErrEndpointWithInvalidNameOrGroup
|
|
||||||
}
|
|
||||||
if len(endpoint.URL) == 0 {
|
|
||||||
return ErrEndpointWithNoURL
|
|
||||||
}
|
|
||||||
if len(endpoint.Conditions) == 0 {
|
|
||||||
return ErrEndpointWithNoCondition
|
return ErrEndpointWithNoCondition
|
||||||
}
|
}
|
||||||
for _, c := range endpoint.Conditions {
|
for _, c := range e.Conditions {
|
||||||
if endpoint.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
if e.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
||||||
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
||||||
}
|
}
|
||||||
if err := c.Validate(); err != nil {
|
if err := c.Validate(); err != nil {
|
||||||
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.DNS != nil {
|
if e.DNSConfig != nil {
|
||||||
return endpoint.DNS.validateAndSetDefault()
|
return e.DNSConfig.ValidateAndSetDefault()
|
||||||
}
|
}
|
||||||
if endpoint.Type() == EndpointTypeUNKNOWN {
|
if e.SSHConfig != nil {
|
||||||
|
return e.SSHConfig.Validate()
|
||||||
|
}
|
||||||
|
if e.Type() == TypeUNKNOWN {
|
||||||
return ErrUnknownEndpointType
|
return ErrUnknownEndpointType
|
||||||
}
|
}
|
||||||
// Make sure that the request can be created
|
// Make sure that the request can be created
|
||||||
_, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body)))
|
_, err := http.NewRequest(e.Method, e.URL, bytes.NewBuffer([]byte(e.Body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if endpoint.SSH != nil {
|
|
||||||
return endpoint.SSH.ValidateAndSetDefaults()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
||||||
func (endpoint Endpoint) DisplayName() string {
|
func (e *Endpoint) DisplayName() string {
|
||||||
if len(endpoint.Group) > 0 {
|
if len(e.Group) > 0 {
|
||||||
return endpoint.Group + "/" + endpoint.Name
|
return e.Group + "/" + e.Name
|
||||||
}
|
}
|
||||||
return endpoint.Name
|
return e.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key returns the unique key for the Endpoint
|
// Key returns the unique key for the Endpoint
|
||||||
func (endpoint Endpoint) Key() string {
|
func (e *Endpoint) Key() string {
|
||||||
return util.ConvertGroupAndEndpointNameToKey(endpoint.Group, endpoint.Name)
|
return ConvertGroupAndEndpointNameToKey(e.Group, e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (e *Endpoint) Close() {
|
||||||
|
if e.Type() == TypeHTTP {
|
||||||
|
client.GetHTTPClient(e.ClientConfig).CloseIdleConnections()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
||||||
func (endpoint *Endpoint) EvaluateHealth() *Result {
|
func (e *Endpoint) EvaluateHealth() *Result {
|
||||||
result := &Result{Success: true, Errors: []string{}}
|
result := &Result{Success: true, Errors: []string{}}
|
||||||
// Parse or extract hostname from URL
|
// Parse or extract hostname from URL
|
||||||
if endpoint.DNS != nil {
|
if e.DNSConfig != nil {
|
||||||
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53")
|
result.Hostname = strings.TrimSuffix(e.URL, ":53")
|
||||||
} else {
|
} else {
|
||||||
urlObject, err := url.Parse(endpoint.URL)
|
urlObject, err := url.Parse(e.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
} else {
|
} else {
|
||||||
@@ -291,11 +264,11 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Retrieve IP if necessary
|
// Retrieve IP if necessary
|
||||||
if endpoint.needsToRetrieveIP() {
|
if e.needsToRetrieveIP() {
|
||||||
endpoint.getIP(result)
|
e.getIP(result)
|
||||||
}
|
}
|
||||||
// Retrieve domain expiration if necessary
|
// Retrieve domain expiration if necessary
|
||||||
if endpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
if e.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
||||||
var err error
|
var err error
|
||||||
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@@ -303,34 +276,37 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
|||||||
}
|
}
|
||||||
// Call the endpoint (if there's no errors)
|
// Call the endpoint (if there's no errors)
|
||||||
if len(result.Errors) == 0 {
|
if len(result.Errors) == 0 {
|
||||||
endpoint.call(result)
|
e.call(result)
|
||||||
} else {
|
} else {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
// Evaluate the conditions
|
// Evaluate the conditions
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
success := condition.evaluate(result, endpoint.UIConfig.DontResolveFailedConditions)
|
success := condition.evaluate(result, e.UIConfig.DontResolveFailedConditions)
|
||||||
if !success {
|
if !success {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.Timestamp = time.Now()
|
result.Timestamp = time.Now()
|
||||||
// Clean up parameters that we don't need to keep in the results
|
// Clean up parameters that we don't need to keep in the results
|
||||||
if endpoint.UIConfig.HideURL {
|
if e.UIConfig.HideURL {
|
||||||
for errIdx, errorString := range result.Errors {
|
for errIdx, errorString := range result.Errors {
|
||||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, endpoint.URL, "<redacted>")
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, e.URL, "<redacted>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig.HideHostname {
|
if e.UIConfig.HideHostname {
|
||||||
for errIdx, errorString := range result.Errors {
|
for errIdx, errorString := range result.Errors {
|
||||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
||||||
}
|
}
|
||||||
result.Hostname = ""
|
result.Hostname = ""
|
||||||
}
|
}
|
||||||
|
if e.UIConfig.HideConditions {
|
||||||
|
result.ConditionResults = nil
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) getIP(result *Result) {
|
func (e *Endpoint) getIP(result *Result) {
|
||||||
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
@@ -339,24 +315,28 @@ func (endpoint *Endpoint) getIP(result *Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) call(result *Result) {
|
func (e *Endpoint) call(result *Result) {
|
||||||
var request *http.Request
|
var request *http.Request
|
||||||
var response *http.Response
|
var response *http.Response
|
||||||
var err error
|
var err error
|
||||||
var certificate *x509.Certificate
|
var certificate *x509.Certificate
|
||||||
endpointType := endpoint.Type()
|
endpointType := e.Type()
|
||||||
if endpointType == EndpointTypeHTTP {
|
if endpointType == TypeHTTP {
|
||||||
request = endpoint.buildHTTPRequest()
|
request = e.buildHTTPRequest()
|
||||||
}
|
}
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
if endpointType == EndpointTypeDNS {
|
if endpointType == TypeDNS {
|
||||||
endpoint.DNS.query(endpoint.URL, result)
|
result.Connected, result.DNSRCode, result.Body, err = client.QueryDNS(e.DNSConfig.QueryType, e.DNSConfig.QueryName, e.URL)
|
||||||
|
if err != nil {
|
||||||
|
result.AddError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS {
|
} else if endpointType == TypeSTARTTLS || endpointType == TypeTLS {
|
||||||
if endpointType == EndpointTypeSTARTTLS {
|
if endpointType == TypeSTARTTLS {
|
||||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(e.URL, "tls://"), e.ClientConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@@ -364,39 +344,39 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||||
} else if endpointType == EndpointTypeTCP {
|
} else if endpointType == TypeTCP {
|
||||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(e.URL, "tcp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeUDP {
|
} else if endpointType == TypeUDP {
|
||||||
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(endpoint.URL, "udp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(e.URL, "udp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSCTP {
|
} else if endpointType == TypeSCTP {
|
||||||
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(endpoint.URL, "sctp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeICMP {
|
} else if endpointType == TypeICMP {
|
||||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
|
||||||
} else if endpointType == EndpointTypeWS {
|
} else if endpointType == TypeWS {
|
||||||
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig)
|
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.Body, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSSH {
|
} else if endpointType == TypeSSH {
|
||||||
var cli *ssh.Client
|
var cli *ssh.Client
|
||||||
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
|
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
|
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.Body, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@@ -410,7 +390,7 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
result.HTTPStatus = response.StatusCode
|
result.HTTPStatus = response.StatusCode
|
||||||
result.Connected = response.StatusCode > 0
|
result.Connected = response.StatusCode > 0
|
||||||
// Only read the Body if there's a condition that uses the BodyPlaceholder
|
// Only read the Body if there's a condition that uses the BodyPlaceholder
|
||||||
if endpoint.needsToReadBody() {
|
if e.needsToReadBody() {
|
||||||
result.Body, err = io.ReadAll(response.Body)
|
result.Body, err = io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError("error reading response body:" + err.Error())
|
result.AddError("error reading response body:" + err.Error())
|
||||||
@@ -419,28 +399,19 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
func (e *Endpoint) buildHTTPRequest() *http.Request {
|
||||||
// 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
|
var bodyBuffer *bytes.Buffer
|
||||||
if endpoint.GraphQL {
|
if e.GraphQL {
|
||||||
graphQlBody := map[string]string{
|
graphQlBody := map[string]string{
|
||||||
"query": endpoint.Body,
|
"query": e.Body,
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(graphQlBody)
|
body, _ := json.Marshal(graphQlBody)
|
||||||
bodyBuffer = bytes.NewBuffer(body)
|
bodyBuffer = bytes.NewBuffer(body)
|
||||||
} else {
|
} else {
|
||||||
bodyBuffer = bytes.NewBuffer([]byte(endpoint.Body))
|
bodyBuffer = bytes.NewBuffer([]byte(e.Body))
|
||||||
}
|
}
|
||||||
request, _ := http.NewRequest(endpoint.Method, endpoint.URL, bodyBuffer)
|
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
||||||
for k, v := range endpoint.Headers {
|
for k, v := range e.Headers {
|
||||||
request.Header.Set(k, v)
|
request.Header.Set(k, v)
|
||||||
if k == HostHeader {
|
if k == HostHeader {
|
||||||
request.Host = v
|
request.Host = v
|
||||||
@@ -450,8 +421,8 @@ func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
||||||
func (endpoint *Endpoint) needsToReadBody() bool {
|
func (e *Endpoint) needsToReadBody() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasBodyPlaceholder() {
|
if condition.hasBodyPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -460,8 +431,8 @@ func (endpoint *Endpoint) needsToReadBody() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
||||||
func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
func (e *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasDomainExpirationPlaceholder() {
|
if condition.hasDomainExpirationPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -470,8 +441,8 @@ func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
||||||
func (endpoint *Endpoint) needsToRetrieveIP() bool {
|
func (e *Endpoint) needsToRetrieveIP() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasIPPlaceholder() {
|
if condition.hasIPPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,7 +13,9 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,6 +94,25 @@ func TestEndpoint(t *testing.T) {
|
|||||||
return &http.Response{StatusCode: http.StatusBadGateway, Body: http.NoBody}
|
return &http.Response{StatusCode: http.StatusBadGateway, Body: http.NoBody}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "failed-status-condition-with-hidden-conditions",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[STATUS] == 200"},
|
||||||
|
UIConfig: &ui.Config{HideConditions: true},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: false,
|
||||||
|
Connected: true,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{}, // Because UIConfig.HideConditions is true, the condition results should not be shown.
|
||||||
|
DomainExpiration: 0, // Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
},
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusBadGateway, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "condition-with-failed-certificate-expiration",
|
Name: "condition-with-failed-certificate-expiration",
|
||||||
Endpoint: Endpoint{
|
Endpoint: Endpoint{
|
||||||
@@ -123,6 +145,7 @@ func TestEndpoint(t *testing.T) {
|
|||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []Condition{"[DOMAIN_EXPIRATION] > 100h"},
|
Conditions: []Condition{"[DOMAIN_EXPIRATION] > 100h"},
|
||||||
|
Interval: 5 * time.Minute,
|
||||||
},
|
},
|
||||||
ExpectedResult: &Result{
|
ExpectedResult: &Result{
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -195,7 +218,10 @@ func TestEndpoint(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
client.InjectHTTPClient(nil)
|
client.InjectHTTPClient(nil)
|
||||||
}
|
}
|
||||||
scenario.Endpoint.ValidateAndSetDefaults()
|
err := scenario.Endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Error("did not expect an error, got", err)
|
||||||
|
}
|
||||||
result := scenario.Endpoint.EvaluateHealth()
|
result := scenario.Endpoint.EvaluateHealth()
|
||||||
if result.Success != scenario.ExpectedResult.Success {
|
if result.Success != scenario.ExpectedResult.Success {
|
||||||
t.Errorf("Expected success to be %v, got %v", scenario.ExpectedResult.Success, result.Success)
|
t.Errorf("Expected success to be %v, got %v", scenario.ExpectedResult.Success, result.Success)
|
||||||
@@ -241,13 +267,13 @@ func TestEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEndpoint_IsEnabled(t *testing.T) {
|
func TestEndpoint_IsEnabled(t *testing.T) {
|
||||||
if !(Endpoint{Enabled: nil}).IsEnabled() {
|
if !(&Endpoint{Enabled: nil}).IsEnabled() {
|
||||||
t.Error("endpoint.IsEnabled() should've returned true, because Enabled was set to nil")
|
t.Error("endpoint.IsEnabled() should've returned true, because Enabled was set to nil")
|
||||||
}
|
}
|
||||||
if value := false; (Endpoint{Enabled: &value}).IsEnabled() {
|
if value := false; (&Endpoint{Enabled: &value}).IsEnabled() {
|
||||||
t.Error("endpoint.IsEnabled() should've returned false, because Enabled was set to false")
|
t.Error("endpoint.IsEnabled() should've returned false, because Enabled was set to false")
|
||||||
}
|
}
|
||||||
if value := true; !(Endpoint{Enabled: &value}).IsEnabled() {
|
if value := true; !(&Endpoint{Enabled: &value}).IsEnabled() {
|
||||||
t.Error("Endpoint.IsEnabled() should've returned true, because Enabled was set to true")
|
t.Error("Endpoint.IsEnabled() should've returned true, because Enabled was set to true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,105 +281,105 @@ func TestEndpoint_IsEnabled(t *testing.T) {
|
|||||||
func TestEndpoint_Type(t *testing.T) {
|
func TestEndpoint_Type(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
URL string
|
URL string
|
||||||
DNS *DNS
|
DNS *dns.Config
|
||||||
SSH *SSH
|
SSH *ssh.Config
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
args args
|
args args
|
||||||
want EndpointType
|
want Type
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "8.8.8.8",
|
URL: "8.8.8.8",
|
||||||
DNS: &DNS{
|
DNS: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: EndpointTypeDNS,
|
want: TypeDNS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "tcp://127.0.0.1:6379",
|
URL: "tcp://127.0.0.1:6379",
|
||||||
},
|
},
|
||||||
want: EndpointTypeTCP,
|
want: TypeTCP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "icmp://example.com",
|
URL: "icmp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeICMP,
|
want: TypeICMP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "sctp://example.com",
|
URL: "sctp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeSCTP,
|
want: TypeSCTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "udp://example.com",
|
URL: "udp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUDP,
|
want: TypeUDP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "starttls://smtp.gmail.com:587",
|
URL: "starttls://smtp.gmail.com:587",
|
||||||
},
|
},
|
||||||
want: EndpointTypeSTARTTLS,
|
want: TypeSTARTTLS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "tls://example.com:443",
|
URL: "tls://example.com:443",
|
||||||
},
|
},
|
||||||
want: EndpointTypeTLS,
|
want: TypeTLS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
},
|
},
|
||||||
want: EndpointTypeHTTP,
|
want: TypeHTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "wss://example.com/",
|
URL: "wss://example.com/",
|
||||||
},
|
},
|
||||||
want: EndpointTypeWS,
|
want: TypeWS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "ws://example.com/",
|
URL: "ws://example.com/",
|
||||||
},
|
},
|
||||||
want: EndpointTypeWS,
|
want: TypeWS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "ssh://example.com:22",
|
URL: "ssh://example.com:22",
|
||||||
SSH: &SSH{
|
SSH: &ssh.Config{
|
||||||
Username: "root",
|
Username: "root",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: EndpointTypeSSH,
|
want: TypeSSH,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "invalid://example.org",
|
URL: "invalid://example.org",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUNKNOWN,
|
want: TypeUNKNOWN,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "no-scheme",
|
URL: "no-scheme",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUNKNOWN,
|
want: TypeUNKNOWN,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(string(tt.want), func(t *testing.T) {
|
t.Run(string(tt.want), func(t *testing.T) {
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
URL: tt.args.URL,
|
URL: tt.args.URL,
|
||||||
DNS: tt.args.DNS,
|
DNSConfig: tt.args.DNS,
|
||||||
}
|
}
|
||||||
if got := endpoint.Type(); got != tt.want {
|
if got := endpoint.Type(); got != tt.want {
|
||||||
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
||||||
@@ -430,7 +456,10 @@ func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
|
|||||||
Timeout: 0,
|
Timeout: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
if endpoint.ClientConfig == nil {
|
if endpoint.ClientConfig == nil {
|
||||||
t.Error("client configuration should've been set to the default configuration")
|
t.Error("client configuration should've been set to the default configuration")
|
||||||
} else {
|
} else {
|
||||||
@@ -450,7 +479,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
|||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "dns-test",
|
Name: "dns-test",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
DNS: &DNS{
|
DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
@@ -460,13 +489,13 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("did not expect an error, got", err)
|
t.Error("did not expect an error, got", err)
|
||||||
}
|
}
|
||||||
if endpoint.DNS.QueryName != "example.com." {
|
if endpoint.DNSConfig.QueryName != "example.com." {
|
||||||
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||||
tests := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
@@ -476,13 +505,13 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
|||||||
name: "fail when has no user",
|
name: "fail when has no user",
|
||||||
username: "",
|
username: "",
|
||||||
password: "password",
|
password: "password",
|
||||||
expectedErr: ErrEndpointWithoutSSHUsername,
|
expectedErr: ssh.ErrEndpointWithoutSSHUsername,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail when has no password",
|
name: "fail when has no password",
|
||||||
username: "username",
|
username: "username",
|
||||||
password: "",
|
password: "",
|
||||||
expectedErr: ErrEndpointWithoutSSHPassword,
|
expectedErr: ssh.ErrEndpointWithoutSSHPassword,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "success when all fields are set",
|
name: "success when all fields are set",
|
||||||
@@ -492,20 +521,20 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, scenario := range scenarios {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "ssh-test",
|
Name: "ssh-test",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: test.username,
|
Username: scenario.username,
|
||||||
Password: test.password,
|
Password: scenario.password,
|
||||||
},
|
},
|
||||||
Conditions: []Condition{Condition("[STATUS] == 0")},
|
Conditions: []Condition{Condition("[STATUS] == 0")},
|
||||||
}
|
}
|
||||||
err := endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
if err != test.expectedErr {
|
if !errors.Is(err, scenario.expectedErr) {
|
||||||
t.Errorf("expected error %v, got %v", test.expectedErr, err)
|
t.Errorf("expected error %v, got %v", scenario.expectedErr, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -575,7 +604,10 @@ func TestEndpoint_buildHTTPRequest(t *testing.T) {
|
|||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []Condition{condition},
|
Conditions: []Condition{condition},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
request := endpoint.buildHTTPRequest()
|
request := endpoint.buildHTTPRequest()
|
||||||
if request.Method != "GET" {
|
if request.Method != "GET" {
|
||||||
t.Error("request.Method should've been GET, but was", request.Method)
|
t.Error("request.Method should've been GET, but was", request.Method)
|
||||||
@@ -598,7 +630,10 @@ func TestEndpoint_buildHTTPRequestWithCustomUserAgent(t *testing.T) {
|
|||||||
"User-Agent": "Test/2.0",
|
"User-Agent": "Test/2.0",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
request := endpoint.buildHTTPRequest()
|
request := endpoint.buildHTTPRequest()
|
||||||
if request.Method != "GET" {
|
if request.Method != "GET" {
|
||||||
t.Error("request.Method should've been GET, but was", request.Method)
|
t.Error("request.Method should've been GET, but was", request.Method)
|
||||||
@@ -622,7 +657,10 @@ func TestEndpoint_buildHTTPRequestWithHostHeader(t *testing.T) {
|
|||||||
"Host": "example.com",
|
"Host": "example.com",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
request := endpoint.buildHTTPRequest()
|
request := endpoint.buildHTTPRequest()
|
||||||
if request.Method != "POST" {
|
if request.Method != "POST" {
|
||||||
t.Error("request.Method should've been POST, but was", request.Method)
|
t.Error("request.Method should've been POST, but was", request.Method)
|
||||||
@@ -649,7 +687,10 @@ func TestEndpoint_buildHTTPRequestWithGraphQLEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
request := endpoint.buildHTTPRequest()
|
request := endpoint.buildHTTPRequest()
|
||||||
if request.Method != "POST" {
|
if request.Method != "POST" {
|
||||||
t.Error("request.Method should've been POST, but was", request.Method)
|
t.Error("request.Method should've been POST, but was", request.Method)
|
||||||
@@ -671,7 +712,10 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
|||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []Condition{condition, bodyCondition},
|
Conditions: []Condition{condition, bodyCondition},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Condition '%s' should have been a success", condition)
|
t.Errorf("Condition '%s' should have been a success", condition)
|
||||||
@@ -699,7 +743,10 @@ func TestIntegrationEvaluateHealthWithErrorAndHideURL(t *testing.T) {
|
|||||||
HideURL: true,
|
HideURL: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if result.Success {
|
if result.Success {
|
||||||
t.Error("Because one of the conditions was invalid, result.Success should have been false")
|
t.Error("Because one of the conditions was invalid, result.Success should have been false")
|
||||||
@@ -714,17 +761,20 @@ func TestIntegrationEvaluateHealthWithErrorAndHideURL(t *testing.T) {
|
|||||||
|
|
||||||
func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||||
conditionSuccess := Condition("[DNS_RCODE] == NOERROR")
|
conditionSuccess := Condition("[DNS_RCODE] == NOERROR")
|
||||||
conditionBody := Condition("[BODY] == 93.184.216.34")
|
conditionBody := Condition("[BODY] == 93.184.215.14")
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "8.8.8.8",
|
URL: "8.8.8.8",
|
||||||
DNS: &DNS{
|
DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
Conditions: []Condition{conditionSuccess, conditionBody},
|
Conditions: []Condition{conditionSuccess, conditionBody},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Conditions '%s' and '%s' should have been a success", conditionSuccess, conditionBody)
|
t.Errorf("Conditions '%s' and '%s' should have been a success", conditionSuccess, conditionBody)
|
||||||
@@ -738,7 +788,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||||
tests := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
endpoint Endpoint
|
endpoint Endpoint
|
||||||
conditions []Condition
|
conditions []Condition
|
||||||
@@ -749,9 +799,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
endpoint: Endpoint{
|
endpoint: Endpoint{
|
||||||
Name: "ssh-success",
|
Name: "ssh-success",
|
||||||
URL: "ssh://localhost",
|
URL: "ssh://localhost",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: "test",
|
Username: "scenario",
|
||||||
Password: "test",
|
Password: "scenario",
|
||||||
},
|
},
|
||||||
Body: "{ \"command\": \"uptime\" }",
|
Body: "{ \"command\": \"uptime\" }",
|
||||||
},
|
},
|
||||||
@@ -763,9 +813,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
endpoint: Endpoint{
|
endpoint: Endpoint{
|
||||||
Name: "ssh-failure",
|
Name: "ssh-failure",
|
||||||
URL: "ssh://localhost",
|
URL: "ssh://localhost",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: "test",
|
Username: "scenario",
|
||||||
Password: "test",
|
Password: "scenario",
|
||||||
},
|
},
|
||||||
Body: "{ \"command\": \"uptime\" }",
|
Body: "{ \"command\": \"uptime\" }",
|
||||||
},
|
},
|
||||||
@@ -774,13 +824,13 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, scenario := range scenarios {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
test.endpoint.ValidateAndSetDefaults()
|
scenario.endpoint.ValidateAndSetDefaults()
|
||||||
test.endpoint.Conditions = test.conditions
|
scenario.endpoint.Conditions = scenario.conditions
|
||||||
result := test.endpoint.EvaluateHealth()
|
result := scenario.endpoint.EvaluateHealth()
|
||||||
if result.Success != test.success {
|
if result.Success != scenario.success {
|
||||||
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
|
t.Errorf("Expected success to be %v, but was %v", scenario.success, result.Success)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -792,7 +842,10 @@ func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
|||||||
URL: "icmp://127.0.0.1",
|
URL: "icmp://127.0.0.1",
|
||||||
Conditions: []Condition{"[CONNECTED] == true"},
|
Conditions: []Condition{"[CONNECTED] == true"},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Conditions '%s' should have been a success", endpoint.Conditions[0])
|
t.Errorf("Conditions '%s' should have been a success", endpoint.Conditions[0])
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Event is something that happens at a specific time
|
// Event is something that happens at a specific time
|
||||||
type Event struct {
|
type Event struct {
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestNewEventFromResult(t *testing.T) {
|
func TestNewEventFromResult(t *testing.T) {
|
||||||
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrExternalEndpointWithNoToken is the error with which Gatus will panic if an external endpoint is configured without a token.
|
||||||
|
ErrExternalEndpointWithNoToken = errors.New("you must specify a token for each external endpoint")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExternalEndpoint is an endpoint whose result is pushed from outside Gatus, which means that
|
||||||
|
// said endpoints are not monitored by Gatus itself; Gatus only displays their results and takes
|
||||||
|
// care of alerting
|
||||||
|
type ExternalEndpoint struct {
|
||||||
|
// Enabled defines whether to enable the monitoring of the endpoint
|
||||||
|
Enabled *bool `yaml:"enabled,omitempty"`
|
||||||
|
|
||||||
|
// Name of the endpoint. Can be anything.
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
|
||||||
|
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
|
||||||
|
Group string `yaml:"group,omitempty"`
|
||||||
|
|
||||||
|
// Token is the bearer token that must be provided through the Authorization header to push results to the endpoint
|
||||||
|
Token string `yaml:"token,omitempty"`
|
||||||
|
|
||||||
|
// Alerts is the alerting configuration for the endpoint in case of failure
|
||||||
|
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
||||||
|
|
||||||
|
// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
|
||||||
|
NumberOfFailuresInARow int `yaml:"-"`
|
||||||
|
|
||||||
|
// NumberOfSuccessesInARow is the number of successful evaluations in a row
|
||||||
|
NumberOfSuccessesInARow int `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSetDefaults validates the ExternalEndpoint and sets the default values
|
||||||
|
func (externalEndpoint *ExternalEndpoint) ValidateAndSetDefaults() error {
|
||||||
|
if err := validateEndpointNameGroupAndAlerts(externalEndpoint.Name, externalEndpoint.Group, externalEndpoint.Alerts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(externalEndpoint.Token) == 0 {
|
||||||
|
return ErrExternalEndpointWithNoToken
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled returns whether the endpoint is enabled or not
|
||||||
|
func (externalEndpoint *ExternalEndpoint) IsEnabled() bool {
|
||||||
|
if externalEndpoint.Enabled == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *externalEndpoint.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
||||||
|
func (externalEndpoint *ExternalEndpoint) DisplayName() string {
|
||||||
|
if len(externalEndpoint.Group) > 0 {
|
||||||
|
return externalEndpoint.Group + "/" + externalEndpoint.Name
|
||||||
|
}
|
||||||
|
return externalEndpoint.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the unique key for the Endpoint
|
||||||
|
func (externalEndpoint *ExternalEndpoint) Key() string {
|
||||||
|
return ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToEndpoint converts the ExternalEndpoint to an Endpoint
|
||||||
|
func (externalEndpoint *ExternalEndpoint) ToEndpoint() *Endpoint {
|
||||||
|
endpoint := &Endpoint{
|
||||||
|
Enabled: externalEndpoint.Enabled,
|
||||||
|
Name: externalEndpoint.Name,
|
||||||
|
Group: externalEndpoint.Group,
|
||||||
|
Alerts: externalEndpoint.Alerts,
|
||||||
|
NumberOfFailuresInARow: externalEndpoint.NumberOfFailuresInARow,
|
||||||
|
NumberOfSuccessesInARow: externalEndpoint.NumberOfSuccessesInARow,
|
||||||
|
}
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExternalEndpoint_ToEndpoint(t *testing.T) {
|
||||||
|
externalEndpoint := &ExternalEndpoint{
|
||||||
|
Name: "name",
|
||||||
|
Group: "group",
|
||||||
|
}
|
||||||
|
convertedEndpoint := externalEndpoint.ToEndpoint()
|
||||||
|
if externalEndpoint.Name != convertedEndpoint.Name {
|
||||||
|
t.Errorf("expected %s, got %s", externalEndpoint.Name, convertedEndpoint.Name)
|
||||||
|
}
|
||||||
|
if externalEndpoint.Group != convertedEndpoint.Group {
|
||||||
|
t.Errorf("expected %s, got %s", externalEndpoint.Group, convertedEndpoint.Group)
|
||||||
|
}
|
||||||
|
if externalEndpoint.Key() != convertedEndpoint.Key() {
|
||||||
|
t.Errorf("expected %s, got %s", externalEndpoint.Key(), convertedEndpoint.Key())
|
||||||
|
}
|
||||||
|
if externalEndpoint.DisplayName() != convertedEndpoint.DisplayName() {
|
||||||
|
t.Errorf("expected %s, got %s", externalEndpoint.DisplayName(), convertedEndpoint.DisplayName())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
// Result of the evaluation of a Endpoint
|
// Result of the evaluation of a Endpoint
|
||||||
type Result struct {
|
type Result struct {
|
||||||
// HTTPStatus is the HTTP response status code
|
// HTTPStatus is the HTTP response status code
|
||||||
HTTPStatus int `json:"status"`
|
HTTPStatus int `json:"status,omitempty"`
|
||||||
|
|
||||||
// DNSRCode is the response code of a DNS query in a human-readable format
|
// DNSRCode is the response code of a DNS query in a human-readable format
|
||||||
//
|
//
|
||||||
@@ -29,8 +29,8 @@ type Result struct {
|
|||||||
// Errors encountered during the evaluation of the Endpoint's health
|
// Errors encountered during the evaluation of the Endpoint's health
|
||||||
Errors []string `json:"errors,omitempty"`
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
|
||||||
// ConditionResults results of the Endpoint's conditions
|
// ConditionResults are the results of each of the Endpoint's Condition
|
||||||
ConditionResults []*ConditionResult `json:"conditionResults"`
|
ConditionResults []*ConditionResult `json:"conditionResults,omitempty"`
|
||||||
|
|
||||||
// Success whether the result signifies a success or not
|
// Success whether the result signifies a success or not
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 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 SSH endpoint")
|
||||||
|
|
||||||
|
// 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 SSH endpoint")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Username string `yaml:"username,omitempty"`
|
||||||
|
Password string `yaml:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the SSH configuration
|
||||||
|
func (cfg *Config) Validate() error {
|
||||||
|
if len(cfg.Username) == 0 {
|
||||||
|
return ErrEndpointWithoutSSHUsername
|
||||||
|
}
|
||||||
|
if len(cfg.Password) == 0 {
|
||||||
|
return ErrEndpointWithoutSSHPassword
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSSH_validate(t *testing.T) {
|
||||||
|
cfg := &Config{}
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Error("expected an error")
|
||||||
|
} else if !errors.Is(err, ErrEndpointWithoutSSHUsername) {
|
||||||
|
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHUsername, err)
|
||||||
|
}
|
||||||
|
cfg.Username = "username"
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Error("expected an error")
|
||||||
|
} else if !errors.Is(err, ErrEndpointWithoutSSHPassword) {
|
||||||
|
t.Errorf("expected error to be '%v', got '%v'", ErrEndpointWithoutSSHPassword, err)
|
||||||
|
}
|
||||||
|
cfg.Password = "password"
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Errorf("expected no error, got '%v'", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "github.com/TwiN/gatus/v5/util"
|
// Status contains the evaluation Results of an Endpoint
|
||||||
|
type Status struct {
|
||||||
// EndpointStatus contains the evaluation Results of an Endpoint
|
|
||||||
type EndpointStatus struct {
|
|
||||||
// Name of the endpoint
|
// Name of the endpoint
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
|
// Group the endpoint is a part of. Used for grouping multiple endpoints together on the front end.
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
|
|
||||||
// Key is the key representing the EndpointStatus
|
// Key of the Endpoint
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
|
||||||
// Results is the list of endpoint evaluation results
|
// Results is the list of endpoint evaluation results
|
||||||
@@ -27,12 +25,12 @@ type EndpointStatus struct {
|
|||||||
Uptime *Uptime `json:"-"`
|
Uptime *Uptime `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEndpointStatus creates a new EndpointStatus
|
// NewStatus creates a new Status
|
||||||
func NewEndpointStatus(group, name string) *EndpointStatus {
|
func NewStatus(group, name string) *Status {
|
||||||
return &EndpointStatus{
|
return &Status{
|
||||||
Name: name,
|
Name: name,
|
||||||
Group: group,
|
Group: group,
|
||||||
Key: util.ConvertGroupAndEndpointNameToKey(group, name),
|
Key: ConvertGroupAndEndpointNameToKey(group, name),
|
||||||
Results: make([]*Result, 0),
|
Results: make([]*Result, 0),
|
||||||
Events: make([]*Event, 0),
|
Events: make([]*Event, 0),
|
||||||
Uptime: NewUptime(),
|
Uptime: NewUptime(),
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewEndpointStatus(t *testing.T) {
|
||||||
|
ep := &Endpoint{Name: "name", Group: "group"}
|
||||||
|
status := NewStatus(ep.Group, ep.Name)
|
||||||
|
if status.Name != ep.Name {
|
||||||
|
t.Errorf("expected %s, got %s", ep.Name, status.Name)
|
||||||
|
}
|
||||||
|
if status.Group != ep.Group {
|
||||||
|
t.Errorf("expected %s, got %s", ep.Group, status.Group)
|
||||||
|
}
|
||||||
|
if status.Key != "group_name" {
|
||||||
|
t.Errorf("expected %s, got %s", "group_name", status.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ package ui
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// Config is the UI configuration for core.Endpoint
|
// Config is the UI configuration for endpoint.Endpoint
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// HideConditions whether to hide the condition results on the UI
|
||||||
|
HideConditions bool `yaml:"hide-conditions"`
|
||||||
|
|
||||||
// HideHostname whether to hide the hostname in the Result
|
// HideHostname whether to hide the hostname in the Result
|
||||||
HideHostname bool `yaml:"hide-hostname"`
|
HideHostname bool `yaml:"hide-hostname"`
|
||||||
|
|
||||||
@@ -29,6 +32,7 @@ var (
|
|||||||
ErrInvalidBadgeResponseTimeConfig = errors.New("invalid response time badge configuration: expected parameter 'response-time' to have 5 ascending numerical values")
|
ErrInvalidBadgeResponseTimeConfig = errors.New("invalid response time badge configuration: expected parameter 'response-time' to have 5 ascending numerical values")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ValidateAndSetDefaults validates the UI configuration and sets the default values
|
||||||
func (config *Config) ValidateAndSetDefaults() error {
|
func (config *Config) ValidateAndSetDefaults() error {
|
||||||
if config.Badge != nil {
|
if config.Badge != nil {
|
||||||
if len(config.Badge.ResponseTime.Thresholds) != 5 {
|
if len(config.Badge.ResponseTime.Thresholds) != 5 {
|
||||||
@@ -51,6 +55,7 @@ func GetDefaultConfig() *Config {
|
|||||||
HideHostname: false,
|
HideHostname: false,
|
||||||
HideURL: false,
|
HideURL: false,
|
||||||
DontResolveFailedConditions: false,
|
DontResolveFailedConditions: false,
|
||||||
|
HideConditions: false,
|
||||||
Badge: &Badge{
|
Badge: &Badge{
|
||||||
ResponseTime: &ResponseTime{
|
ResponseTime: &ResponseTime{
|
||||||
Thresholds: []int{50, 200, 300, 500, 750},
|
Thresholds: []int{50, 200, 300, 500, 750},
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateAndSetDefaults(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with-valid-config",
|
||||||
|
config: &Config{
|
||||||
|
Badge: &Badge{
|
||||||
|
ResponseTime: &ResponseTime{Thresholds: []int{50, 200, 300, 500, 750}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-invalid-threshold-length",
|
||||||
|
config: &Config{
|
||||||
|
Badge: &Badge{
|
||||||
|
ResponseTime: &ResponseTime{Thresholds: []int{50, 200, 300, 500}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: ErrInvalidBadgeResponseTimeConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-invalid-thresholds-order",
|
||||||
|
config: &Config{
|
||||||
|
Badge: &Badge{ResponseTime: &ResponseTime{Thresholds: []int{50, 200, 500, 300, 750}}},
|
||||||
|
},
|
||||||
|
wantErr: ErrInvalidBadgeResponseTimeConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-no-badge-configured", // should give default badge cfg
|
||||||
|
config: &Config{},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.config.ValidateAndSetDefaults(); !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("Expected error %v, got %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
// Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself
|
// Uptime is the struct that contains the relevant data for calculating the uptime as well as the uptime itself
|
||||||
// and some other statistics
|
// and some other statistics
|
||||||
@@ -1 +0,0 @@
|
|||||||
TODO: move files from core to here.
|
|
||||||
+27
-3
@@ -13,10 +13,16 @@ const (
|
|||||||
|
|
||||||
// DefaultPort is the default port the application will listen on
|
// DefaultPort is the default port the application will listen on
|
||||||
DefaultPort = 8080
|
DefaultPort = 8080
|
||||||
|
|
||||||
|
// DefaultReadBufferSize is the default value for ReadBufferSize
|
||||||
|
DefaultReadBufferSize = 8192
|
||||||
|
|
||||||
|
// MinimumReadBufferSize is the minimum value for ReadBufferSize, and also the default value set
|
||||||
|
// for fiber.Config.ReadBufferSize
|
||||||
|
MinimumReadBufferSize = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the structure which supports the configuration of the endpoint
|
// Config is the structure which supports the configuration of the server listening to requests
|
||||||
// which provides access to the web frontend
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Address to listen on (defaults to 0.0.0.0 specified by DefaultAddress)
|
// Address to listen on (defaults to 0.0.0.0 specified by DefaultAddress)
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
@@ -24,6 +30,14 @@ type Config struct {
|
|||||||
// Port to listen on (default to 8080 specified by DefaultPort)
|
// Port to listen on (default to 8080 specified by DefaultPort)
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
|
|
||||||
|
// ReadBufferSize sets fiber.Config.ReadBufferSize, which is the buffer size for reading requests coming from a
|
||||||
|
// single connection and also acts as a limit for the maximum header size.
|
||||||
|
//
|
||||||
|
// If you're getting occasional "Request Header Fields Too Large", you may want to try increasing this value.
|
||||||
|
//
|
||||||
|
// Defaults to DefaultReadBufferSize
|
||||||
|
ReadBufferSize int `yaml:"read-buffer-size,omitempty"`
|
||||||
|
|
||||||
// TLS configuration (optional)
|
// TLS configuration (optional)
|
||||||
TLS *TLSConfig `yaml:"tls,omitempty"`
|
TLS *TLSConfig `yaml:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -38,7 +52,11 @@ type TLSConfig struct {
|
|||||||
|
|
||||||
// GetDefaultConfig returns a Config struct with the default values
|
// GetDefaultConfig returns a Config struct with the default values
|
||||||
func GetDefaultConfig() *Config {
|
func GetDefaultConfig() *Config {
|
||||||
return &Config{Address: DefaultAddress, Port: DefaultPort}
|
return &Config{
|
||||||
|
Address: DefaultAddress,
|
||||||
|
Port: DefaultPort,
|
||||||
|
ReadBufferSize: DefaultReadBufferSize,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSetDefaults validates the web configuration and sets the default values if necessary.
|
// ValidateAndSetDefaults validates the web configuration and sets the default values if necessary.
|
||||||
@@ -53,6 +71,12 @@ func (web *Config) ValidateAndSetDefaults() error {
|
|||||||
} else if web.Port < 0 || web.Port > math.MaxUint16 {
|
} else if web.Port < 0 || web.Port > math.MaxUint16 {
|
||||||
return fmt.Errorf("invalid port: value should be between %d and %d", 0, math.MaxUint16)
|
return fmt.Errorf("invalid port: value should be between %d and %d", 0, math.MaxUint16)
|
||||||
}
|
}
|
||||||
|
// Validate ReadBufferSize
|
||||||
|
if web.ReadBufferSize == 0 {
|
||||||
|
web.ReadBufferSize = DefaultReadBufferSize // Not set? Use the default value.
|
||||||
|
} else if web.ReadBufferSize < MinimumReadBufferSize {
|
||||||
|
web.ReadBufferSize = MinimumReadBufferSize // Below the minimum? Use the minimum value.
|
||||||
|
}
|
||||||
// Try to load the TLS certificates
|
// Try to load the TLS certificates
|
||||||
if web.TLS != nil {
|
if web.TLS != nil {
|
||||||
if err := web.TLS.isValid(); err != nil {
|
if err := web.TLS.isValid(); err != nil {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user