mirror of
https://github.com/TwiN/gatus.git
synced 2026-08-22 15:16:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4d9821f3 | ||
|
|
d01a5d418b | ||
|
|
34f8cd1eca | ||
|
|
d101c17136 | ||
|
|
ade3d05983 | ||
|
|
fbab0ef7ca | ||
|
|
9121ec1cc8 | ||
|
|
6ddf1258e5 | ||
|
|
490610ccfd | ||
|
|
0eb6958085 | ||
|
|
d20a41c7a7 | ||
|
|
4c18e0d602 | ||
|
|
da24b7e8ac | ||
|
|
c619066e25 | ||
|
|
3688dd6e6f | ||
|
|
fc778300be | ||
|
|
df560ad872 | ||
|
|
de9c366777 | ||
|
|
6a5fec2c55 | ||
|
|
01d2ed3f02 | ||
|
|
92b85ee1ab | ||
|
|
a789deb8c2 | ||
|
|
e5a94979dd | ||
|
|
3c0ea72a5c | ||
|
|
d17e893131 | ||
|
|
7ea34ec8a8 | ||
|
|
f6b99f34db | ||
|
|
37495ac3f3 | ||
|
|
557f696f88 | ||
|
|
c86492dbfd |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 48 KiB |
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.19
|
||||
repository: "${{ github.event.inputs.repository }}"
|
||||
ref: "${{ github.event.inputs.ref }}"
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build binary to make sure it works
|
||||
run: go build -mod vendor
|
||||
|
||||
@@ -19,10 +19,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
pull: true
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_REPOSITORY }}:experimental
|
||||
tags: ${{ env.IMAGE_REPOSITORY }}:experimental
|
||||
|
||||
@@ -4,6 +4,9 @@ on:
|
||||
workflows: ["build"]
|
||||
branches: [master]
|
||||
types: [completed]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
publish-latest:
|
||||
name: publish-latest
|
||||
@@ -24,10 +27,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push docker image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
pull: true
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_REPOSITORY }}:latest
|
||||
tags: ${{ env.IMAGE_REPOSITORY }}:latest
|
||||
|
||||
@@ -24,10 +24,13 @@ docker run -p 8080:8080 --name gatus twinproduction/gatus
|
||||
For more details, see [Usage](#usage)
|
||||
</details>
|
||||
|
||||

|
||||

|
||||
|
||||
Have any feedback or questions? [Create a discussion](https://github.com/TwiN/gatus/discussions/new).
|
||||
|
||||
Like this project? Please consider [sponsoring me](https://github.com/sponsors/TwiN).
|
||||
|
||||
|
||||
## Table of Contents
|
||||
- [Why Gatus?](#why-gatus)
|
||||
- [Features](#features)
|
||||
@@ -80,6 +83,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
||||
- [Endpoint groups](#endpoint-groups)
|
||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||
- [Keeping your configuration small](#keeping-your-configuration-small)
|
||||
- [Badges](#badges)
|
||||
- [Uptime](#uptime)
|
||||
- [Health](#health)
|
||||
@@ -88,6 +92,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [High level design overview](#high-level-design-overview)
|
||||
- [Sponsors](#sponsors)
|
||||
|
||||
|
||||
## Why Gatus?
|
||||
Before getting into the specifics, I want to address the most common question:
|
||||
> Why would I use Gatus when I can just use Prometheus’ Alertmanager, Cloudwatch or even Splunk?
|
||||
@@ -117,7 +122,8 @@ The main features of Gatus are:
|
||||
- **[Badges](#badges)**:  
|
||||
- **Dark mode**
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Usage
|
||||
By default, the configuration file is expected to be at `config/config.yaml`.
|
||||
@@ -818,6 +824,7 @@ Here's an example of what the notifications look like:
|
||||
|
||||

|
||||
|
||||
|
||||
#### Configuring Telegram alerts
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------|:-------------------------------------------------------------------------------------------|:---------------------------|
|
||||
@@ -886,6 +893,7 @@ endpoints:
|
||||
description: "healthcheck failed"
|
||||
```
|
||||
|
||||
|
||||
#### Configuring custom alerts
|
||||
| Parameter | Description | Default |
|
||||
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
@@ -1046,6 +1054,7 @@ endpoints:
|
||||
- type: pagerduty
|
||||
```
|
||||
|
||||
|
||||
### Maintenance
|
||||
If you have maintenance windows, you may not want to be annoyed by alerts.
|
||||
To do that, you'll have to use the maintenance configuration:
|
||||
@@ -1084,6 +1093,7 @@ maintenance:
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||
|
||||
|
||||
#### Basic
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------|:-----------------------------------------------------------------------------------|:--------------|
|
||||
@@ -1102,6 +1112,7 @@ security:
|
||||
**WARNING:** 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 2022-01-08, I suggest a cost of 8.
|
||||
|
||||
|
||||
#### OIDC
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------|:---------------------------------------------------------------|:--------------|
|
||||
@@ -1215,7 +1226,6 @@ and [helmfile example](https://github.com/avakarev/gatus-chart#helmfileyaml-exam
|
||||
Gatus can be deployed on Terraform by using the following module: [terraform-kubernetes-gatus](https://github.com/TwiN/terraform-kubernetes-gatus).
|
||||
|
||||
|
||||
|
||||
## Running the tests
|
||||
```console
|
||||
go test ./... -mod vendor
|
||||
@@ -1504,6 +1514,43 @@ web:
|
||||
```
|
||||
|
||||
|
||||
### Keeping your configuration small
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary>Example</summary>
|
||||
|
||||
```yaml
|
||||
default-endpoint: &defaults
|
||||
group: core
|
||||
interval: 5m
|
||||
client:
|
||||
insecure: true
|
||||
timeout: 30s
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
endpoints:
|
||||
- name: anchor-example-1
|
||||
<<: *defaults # This will merge the configuration under &defaults with this endpoint
|
||||
url: "https://example.org"
|
||||
|
||||
- name: anchor-example-2
|
||||
<<: *defaults
|
||||
group: example # This will override the group defined in &defaults
|
||||
url: "https://example.com"
|
||||
|
||||
- name: anchor-example-3
|
||||
<<: *defaults
|
||||
url: "https://twin.sh/health"
|
||||
conditions: # This will override the conditions defined in &defaults
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
### Badges
|
||||
#### Uptime
|
||||

|
||||
@@ -1568,6 +1615,7 @@ Where:
|
||||
- `{duration}` is `7d`, `24h` or `1h`
|
||||
- `{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
|
||||
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]
|
||||
|
||||
@@ -84,6 +84,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
|
||||
@@ -2,6 +2,7 @@ package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -44,7 +45,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,6 +55,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -61,8 +63,26 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Content string `json:"content"`
|
||||
Embeds []Embed `json:"embeds"`
|
||||
}
|
||||
|
||||
type Embed struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Color int `json:"color"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Inline bool `json:"inline"`
|
||||
}
|
||||
|
||||
// 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(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, results string
|
||||
var colorCode int
|
||||
if resolved {
|
||||
@@ -79,29 +99,30 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
} else {
|
||||
prefix = ":x:"
|
||||
}
|
||||
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
description = ":\n> " + alertDescription
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"content": "",
|
||||
"embeds": [
|
||||
{
|
||||
"title": ":helmet_with_white_cross: Gatus",
|
||||
"description": "%s%s",
|
||||
"color": %d,
|
||||
"fields": [
|
||||
{
|
||||
"name": "Condition results",
|
||||
"value": "%s",
|
||||
"inline": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, message, description, colorCode, results)
|
||||
body, _ := json.Marshal(Body{
|
||||
Content: "",
|
||||
Embeds: []Embed{
|
||||
{
|
||||
Title: ":helmet_with_white_cross: Gatus",
|
||||
Description: message + description,
|
||||
Color: colorCode,
|
||||
Fields: []Field{
|
||||
{
|
||||
Name: "Condition results",
|
||||
Value: results,
|
||||
Inline: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
|
||||
@@ -151,14 +151,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"content\": \"\",\n \"embeds\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"description\": \"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"color\": 15158332,\n \"fields\": [\n {\n \"name\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"inline\": false\n }\n ]\n }\n ]\n}",
|
||||
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"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: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"content\": \"\",\n \"embeds\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"description\": \"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"color\": 3066993,\n \"fields\": [\n {\n \"name\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"inline\": false\n }\n ]\n }\n ]\n}",
|
||||
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"color\":3066993,\"fields\":[{\"name\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n:white_check_mark: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -170,15 +170,16 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -62,6 +62,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
|
||||
@@ -177,7 +177,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
b, _ := json.Marshal(body)
|
||||
e, _ := json.Marshal(scenario.ExpectedBody)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", e, b)
|
||||
t.Errorf("expected:\n%s\ngot:\n%s", e, b)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package matrix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
@@ -61,7 +62,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
config := provider.getConfigForGroup(endpoint.Group)
|
||||
if config.ServerURL == "" {
|
||||
config.ServerURL = defaultHomeserverURL
|
||||
@@ -86,6 +87,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -93,17 +95,22 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Format string `json:"format"`
|
||||
Body string `json:"body"`
|
||||
FormattedBody string `json:"formatted_body"`
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
return fmt.Sprintf(`{
|
||||
"msgtype": "m.text",
|
||||
"format": "org.matrix.custom.html",
|
||||
"body": "%s",
|
||||
"formatted_body": "%s"
|
||||
}`,
|
||||
buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
||||
buildHTMLMessageBody(endpoint, alert, result, resolved),
|
||||
)
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
body, _ := json.Marshal(Body{
|
||||
MsgType: "m.text",
|
||||
Format: "org.matrix.custom.html",
|
||||
Body: buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
||||
FormattedBody: buildHTMLMessageBody(endpoint, alert, result, resolved),
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
||||
@@ -121,13 +128,13 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu
|
||||
} else {
|
||||
prefix = "✕"
|
||||
}
|
||||
results += fmt.Sprintf("\\n%s - %s", prefix, conditionResult.Condition)
|
||||
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
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, results)
|
||||
}
|
||||
|
||||
// buildHTMLMessageBody builds the message body in HTML to include in request
|
||||
@@ -149,9 +156,9 @@ func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *c
|
||||
}
|
||||
var description string
|
||||
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\n<h5>Condition results</h5><ul>%s</ul>", message, description, results)
|
||||
}
|
||||
|
||||
// getConfigForGroup returns the appropriate configuration for a given group
|
||||
|
||||
@@ -184,14 +184,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n\t\"msgtype\": \"m.text\",\n\t\"format\": \"org.matrix.custom.html\",\n\t\"body\": \"An alert for `endpoint-name` has been triggered due to having failed 3 time(s) in a row\\ndescription-1\\n\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\n\t\"formatted_body\": \"<h3>An alert for <code>endpoint-name</code> has been triggered due to having failed 3 time(s) in a row</h3>\\n<blockquote>description-1</blockquote>\\n<h5>Condition results</h5><ul><li>❌ - <code>[CONNECTED] == true</code></li><li>❌ - <code>[STATUS] == 200</code></li></ul>\"\n}",
|
||||
ExpectedBody: "{\"msgtype\":\"m.text\",\"format\":\"org.matrix.custom.html\",\"body\":\"An alert for `endpoint-name` has been triggered due to having failed 3 time(s) in a row\\ndescription-1\\n\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"formatted_body\":\"\\u003ch3\\u003eAn alert for \\u003ccode\\u003eendpoint-name\\u003c/code\\u003e has been triggered due to having failed 3 time(s) in a row\\u003c/h3\\u003e\\n\\u003cblockquote\\u003edescription-1\\u003c/blockquote\\u003e\\n\\u003ch5\\u003eCondition results\\u003c/h5\\u003e\\u003cul\\u003e\\u003cli\\u003e❌ - \\u003ccode\\u003e[CONNECTED] == true\\u003c/code\\u003e\\u003c/li\\u003e\\u003cli\\u003e❌ - \\u003ccode\\u003e[STATUS] == 200\\u003c/code\\u003e\\u003c/li\\u003e\\u003c/ul\\u003e\"}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n\t\"msgtype\": \"m.text\",\n\t\"format\": \"org.matrix.custom.html\",\n\t\"body\": \"An alert for `endpoint-name` has been resolved after passing successfully 5 time(s) in a row\\ndescription-2\\n\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\n\t\"formatted_body\": \"<h3>An alert for <code>endpoint-name</code> has been resolved after passing successfully 5 time(s) in a row</h3>\\n<blockquote>description-2</blockquote>\\n<h5>Condition results</h5><ul><li>✅ - <code>[CONNECTED] == true</code></li><li>✅ - <code>[STATUS] == 200</code></li></ul>\"\n}",
|
||||
ExpectedBody: "{\"msgtype\":\"m.text\",\"format\":\"org.matrix.custom.html\",\"body\":\"An alert for `endpoint-name` has been resolved after passing successfully 5 time(s) in a row\\ndescription-2\\n\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"formatted_body\":\"\\u003ch3\\u003eAn alert for \\u003ccode\\u003eendpoint-name\\u003c/code\\u003e has been resolved after passing successfully 5 time(s) in a row\\u003c/h3\\u003e\\n\\u003cblockquote\\u003edescription-2\\u003c/blockquote\\u003e\\n\\u003ch5\\u003eCondition results\\u003c/h5\\u003e\\u003cul\\u003e\\u003cli\\u003e✅ - \\u003ccode\\u003e[CONNECTED] == true\\u003c/code\\u003e\\u003c/li\\u003e\\u003cli\\u003e✅ - \\u003ccode\\u003e[STATUS] == 200\\u003c/code\\u003e\\u003c/li\\u003e\\u003c/ul\\u003e\"}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -207,11 +207,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package mattermost
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -60,6 +61,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -67,9 +69,31 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Title string `json:"title"`
|
||||
Fallback string `json:"fallback"`
|
||||
Text string `json:"text"`
|
||||
Short bool `json:"short"`
|
||||
Color string `json:"color"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Title string `json:"title"`
|
||||
Value string `json:"value"`
|
||||
Short bool `json:"short"`
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, color string
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, color, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
color = "#36A64F"
|
||||
@@ -77,7 +101,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
color = "#DD0000"
|
||||
}
|
||||
var results string
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
@@ -85,38 +108,34 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
} else {
|
||||
prefix = ":x:"
|
||||
}
|
||||
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
description = ":\n> " + alertDescription
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"text": "",
|
||||
"username": "gatus",
|
||||
"icon_url": "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
||||
"attachments": [
|
||||
{
|
||||
"title": ":rescue_worker_helmet: Gatus",
|
||||
"fallback": "Gatus - %s",
|
||||
"text": "%s%s",
|
||||
"short": false,
|
||||
"color": "%s",
|
||||
"fields": [
|
||||
{
|
||||
"title": "URL",
|
||||
"value": "%s",
|
||||
"short": false
|
||||
},
|
||||
{
|
||||
"title": "Condition results",
|
||||
"value": "%s",
|
||||
"short": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, message, message, description, color, endpoint.URL, results)
|
||||
body, _ := json.Marshal(Body{
|
||||
Text: "",
|
||||
Username: "gatus",
|
||||
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
||||
Attachments: []Attachment{
|
||||
{
|
||||
Title: ":helmet_with_white_cross: Gatus",
|
||||
Fallback: "Gatus - " + message,
|
||||
Text: message + description,
|
||||
Short: false,
|
||||
Color: color,
|
||||
Fields: []Field{
|
||||
{
|
||||
Title: "Condition results",
|
||||
Value: results,
|
||||
Short: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
|
||||
@@ -155,14 +155,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"username\": \"gatus\",\n \"icon_url\": \"https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png\",\n \"attachments\": [\n {\n \"title\": \":rescue_worker_helmet: Gatus\",\n \"fallback\": \"Gatus - An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row\",\n \"text\": \"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"URL\",\n \"value\": \"\",\n \"short\": false\n },\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
ExpectedBody: "{\"text\":\"\",\"username\":\"gatus\",\"icon_url\":\"https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"fallback\":\"Gatus - An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row\",\"text\":\"An alert for *endpoint-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: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"username\": \"gatus\",\n \"icon_url\": \"https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png\",\n \"attachments\": [\n {\n \"title\": \":rescue_worker_helmet: Gatus\",\n \"fallback\": \"Gatus - An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row\",\n \"text\": \"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"URL\",\n \"value\": \"\",\n \"short\": false\n },\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
ExpectedBody: "{\"text\":\"\",\"username\":\"gatus\",\"icon_url\":\"https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"fallback\":\"Gatus - An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row\",\"text\":\"An alert for *endpoint-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}]}]}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -178,11 +178,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package messagebird
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -33,7 +34,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
// Send an alert using the provider
|
||||
// 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 {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -44,6 +45,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -51,19 +53,26 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Originator string `json:"originator"`
|
||||
Recipients string `json:"recipients"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// 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(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"originator": "%s",
|
||||
"recipients": "%s",
|
||||
"body": "%s"
|
||||
}`, provider.Originator, provider.Recipients, message)
|
||||
body, _ := json.Marshal(Body{
|
||||
Originator: provider.Originator,
|
||||
Recipients: provider.Recipients,
|
||||
Body: message,
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
||||
@@ -118,14 +118,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{AccessKey: "1", Originator: "2", Recipients: "3"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"originator\": \"2\",\n \"recipients\": \"3\",\n \"body\": \"TRIGGERED: endpoint-name - description-1\"\n}",
|
||||
ExpectedBody: "{\"originator\":\"2\",\"recipients\":\"3\",\"body\":\"TRIGGERED: endpoint-name - description-1\"}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{AccessKey: "4", Originator: "5", Recipients: "6"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"originator\": \"5\",\n \"recipients\": \"6\",\n \"body\": \"RESOLVED: endpoint-name - description-2\"\n}",
|
||||
ExpectedBody: "{\"originator\":\"5\",\"recipients\":\"6\",\"body\":\"RESOLVED: endpoint-name - description-2\"}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -141,8 +141,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package ntfy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -39,7 +40,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -49,6 +50,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -56,8 +58,16 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Topic string `json:"topic"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Tags []string `json:"tags"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, tag string
|
||||
if len(alert.GetDescription()) > 0 {
|
||||
message = endpoint.DisplayName() + " - " + alert.GetDescription()
|
||||
@@ -69,13 +79,14 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
} else {
|
||||
tag = "x"
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"topic": "%s",
|
||||
"title": "Gatus",
|
||||
"message": "%s",
|
||||
"tags": ["%s"],
|
||||
"priority": %d
|
||||
}`, provider.Topic, message, tag, provider.Priority)
|
||||
body, _ := json.Marshal(Body{
|
||||
Topic: provider.Topic,
|
||||
Title: "Gatus",
|
||||
Message: message,
|
||||
Tags: []string{tag},
|
||||
Priority: provider.Priority,
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
||||
@@ -69,14 +69,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"topic\": \"example\",\n \"title\": \"Gatus\",\n \"message\": \"endpoint-name - description-1\",\n \"tags\": [\"x\"],\n \"priority\": 1\n}",
|
||||
ExpectedBody: "{\"topic\":\"example\",\"title\":\"Gatus\",\"message\":\"endpoint-name - description-1\",\"tags\":[\"x\"],\"priority\":1}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"topic\": \"example\",\n \"title\": \"Gatus\",\n \"message\": \"endpoint-name - description-2\",\n \"tags\": [\"white_check_mark\"],\n \"priority\": 2\n}",
|
||||
ExpectedBody: "{\"topic\":\"example\",\"title\":\"Gatus\",\"message\":\"endpoint-name - description-2\",\"tags\":[\"white_check_mark\"],\"priority\":2}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -92,11 +92,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -83,37 +83,36 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
|
||||
func (provider *AlertProvider) createAlert(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
payload := provider.buildCreateRequestBody(endpoint, alert, result, resolved)
|
||||
_, err := provider.sendRequest(restAPI, http.MethodPost, payload)
|
||||
return err
|
||||
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) closeAlert(endpoint *core.Endpoint, alert *alert.Alert) error {
|
||||
payload := provider.buildCloseRequestBody(endpoint, alert)
|
||||
url := restAPI + "/" + provider.alias(buildKey(endpoint)) + "/close?identifierType=alias"
|
||||
_, err := provider.sendRequest(url, http.MethodPost, payload)
|
||||
return err
|
||||
return provider.sendRequest(url, http.MethodPost, payload)
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) sendRequest(url, method string, payload interface{}) (*http.Response, error) {
|
||||
func (provider *AlertProvider) sendRequest(url, method string, payload interface{}) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to build alert payload: %v", payload)
|
||||
return fmt.Errorf("error build alert with payload %v: %w", payload, err)
|
||||
}
|
||||
request, err := http.NewRequest(method, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Authorization", "GenieKey "+provider.APIKey)
|
||||
res, err := client.GetHTTPClient(nil).Do(request)
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if res.StatusCode > 399 {
|
||||
rBody, _ := io.ReadAll(res.Body)
|
||||
return nil, fmt.Errorf("call to provider alert returned status code %d: %s", res.StatusCode, string(rBody))
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode > 399 {
|
||||
rBody, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(rBody))
|
||||
}
|
||||
return res, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) alertCreateRequest {
|
||||
|
||||
@@ -53,7 +53,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
//
|
||||
// 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 {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -63,6 +63,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -86,8 +87,21 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return nil
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
RoutingKey string `json:"routing_key"`
|
||||
DedupKey string `json:"dedup_key"`
|
||||
EventAction string `json:"event_action"`
|
||||
Payload Payload `json:"payload"`
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Summary string `json:"summary"`
|
||||
Source string `json:"source"`
|
||||
Severity string `json:"severity"`
|
||||
}
|
||||
|
||||
// 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(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, eventAction, resolveKey string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||
@@ -98,16 +112,17 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
eventAction = "trigger"
|
||||
resolveKey = ""
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"routing_key": "%s",
|
||||
"dedup_key": "%s",
|
||||
"event_action": "%s",
|
||||
"payload": {
|
||||
"summary": "%s",
|
||||
"source": "%s",
|
||||
"severity": "critical"
|
||||
}
|
||||
}`, provider.getIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name)
|
||||
body, _ := json.Marshal(Body{
|
||||
RoutingKey: provider.getIntegrationKeyForGroup(endpoint.Group),
|
||||
DedupKey: resolveKey,
|
||||
EventAction: eventAction,
|
||||
Payload: Payload{
|
||||
Summary: message,
|
||||
Source: "Gatus",
|
||||
Severity: "critical",
|
||||
},
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// getIntegrationKeyForGroup returns the appropriate pagerduty integration key for a given group
|
||||
|
||||
@@ -149,24 +149,24 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{IntegrationKey: "00000000000000000000000000000000"},
|
||||
Alert: alert.Alert{Description: &description},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"routing_key\": \"00000000000000000000000000000000\",\n \"dedup_key\": \"\",\n \"event_action\": \"trigger\",\n \"payload\": {\n \"summary\": \"TRIGGERED: - test\",\n \"source\": \"\",\n \"severity\": \"critical\"\n }\n}",
|
||||
ExpectedBody: "{\"routing_key\":\"00000000000000000000000000000000\",\"dedup_key\":\"\",\"event_action\":\"trigger\",\"payload\":{\"summary\":\"TRIGGERED: endpoint-name - test\",\"source\":\"Gatus\",\"severity\":\"critical\"}}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{IntegrationKey: "00000000000000000000000000000000"},
|
||||
Alert: alert.Alert{Description: &description, ResolveKey: "key"},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"routing_key\": \"00000000000000000000000000000000\",\n \"dedup_key\": \"key\",\n \"event_action\": \"resolve\",\n \"payload\": {\n \"summary\": \"RESOLVED: - test\",\n \"source\": \"\",\n \"severity\": \"critical\"\n }\n}",
|
||||
ExpectedBody: "{\"routing_key\":\"00000000000000000000000000000000\",\"dedup_key\":\"key\",\"event_action\":\"resolve\",\"payload\":{\"summary\":\"RESOLVED: endpoint-name - test\",\"source\":\"Gatus\",\"severity\":\"critical\"}}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(&core.Endpoint{}, &scenario.Alert, &core.Result{}, scenario.Resolved)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
body := scenario.Provider.buildRequestBody(&core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &core.Result{}, 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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package slack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -42,7 +43,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -52,6 +53,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -59,8 +61,27 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Text string `json:"text"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Short bool `json:"short"`
|
||||
Color string `json:"color"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Title string `json:"title"`
|
||||
Value string `json:"value"`
|
||||
Short bool `json:"short"`
|
||||
}
|
||||
|
||||
// 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(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, color, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
@@ -76,30 +97,31 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
} else {
|
||||
prefix = ":x:"
|
||||
}
|
||||
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
description = ":\n> " + alertDescription
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"text": "",
|
||||
"attachments": [
|
||||
{
|
||||
"title": ":helmet_with_white_cross: Gatus",
|
||||
"text": "%s%s",
|
||||
"short": false,
|
||||
"color": "%s",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Condition results",
|
||||
"value": "%s",
|
||||
"short": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, message, description, color, results)
|
||||
body, _ := json.Marshal(Body{
|
||||
Text: "",
|
||||
Attachments: []Attachment{
|
||||
{
|
||||
Title: ":helmet_with_white_cross: Gatus",
|
||||
Text: message + description,
|
||||
Short: false,
|
||||
Color: color,
|
||||
Fields: []Field{
|
||||
{
|
||||
Title: "Condition results",
|
||||
Value: results,
|
||||
Short: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
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}]}]}",
|
||||
},
|
||||
{
|
||||
Name: "triggered-with-group",
|
||||
@@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
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: "resolved",
|
||||
@@ -169,7 +169,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Endpoint: core.Endpoint{Name: "name"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
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}]}]}",
|
||||
},
|
||||
{
|
||||
Name: "resolved-with-group",
|
||||
@@ -177,7 +177,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
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}]}]}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -193,11 +193,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package teams
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -44,7 +45,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,6 +55,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -61,8 +63,22 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Type string `json:"@type"`
|
||||
Context string `json:"@context"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Sections []Section `json:"sections"`
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
ActivityTitle string `json:"activityTitle"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// 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(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, color string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
@@ -83,25 +99,22 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
description = ": " + alertDescription
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": "%s",
|
||||
"title": "🚨 Gatus",
|
||||
"text": "%s%s",
|
||||
"sections": [
|
||||
{
|
||||
"activityTitle": "URL",
|
||||
"text": "%s"
|
||||
},
|
||||
{
|
||||
"activityTitle": "Condition results",
|
||||
"text": "%s"
|
||||
}
|
||||
]
|
||||
}`, color, message, description, endpoint.URL, results)
|
||||
body, _ := json.Marshal(Body{
|
||||
Type: "MessageCard",
|
||||
Context: "http://schema.org/extensions",
|
||||
ThemeColor: color,
|
||||
Title: "🚨 Gatus",
|
||||
Text: message + description,
|
||||
Sections: []Section{
|
||||
{
|
||||
ActivityTitle: "Condition results",
|
||||
Text: results,
|
||||
},
|
||||
},
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
|
||||
@@ -151,14 +151,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"@type\": \"MessageCard\",\n \"@context\": \"http://schema.org/extensions\",\n \"themeColor\": \"#DD0000\",\n \"title\": \"🚨 Gatus\",\n \"text\": \"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"sections\": [\n {\n \"activityTitle\": \"URL\",\n \"text\": \"\"\n },\n {\n \"activityTitle\": \"Condition results\",\n \"text\": \"❌ - `[CONNECTED] == true`<br/>❌ - `[STATUS] == 200`<br/>\"\n }\n ]\n}",
|
||||
ExpectedBody: "{\"@type\":\"MessageCard\",\"@context\":\"http://schema.org/extensions\",\"themeColor\":\"#DD0000\",\"title\":\"\\u0026#x1F6A8; Gatus\",\"text\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row: description-1\",\"sections\":[{\"activityTitle\":\"Condition results\",\"text\":\"\\u0026#x274C; - `[CONNECTED] == true`\\u003cbr/\\u003e\\u0026#x274C; - `[STATUS] == 200`\\u003cbr/\\u003e\"}]}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"@type\": \"MessageCard\",\n \"@context\": \"http://schema.org/extensions\",\n \"themeColor\": \"#36A64F\",\n \"title\": \"🚨 Gatus\",\n \"text\": \"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"sections\": [\n {\n \"activityTitle\": \"URL\",\n \"text\": \"\"\n },\n {\n \"activityTitle\": \"Condition results\",\n \"text\": \"✅ - `[CONNECTED] == true`<br/>✅ - `[STATUS] == 200`<br/>\"\n }\n ]\n}",
|
||||
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\"}]}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -174,11 +174,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -36,7 +37,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
apiURL := provider.APIURL
|
||||
if apiURL == "" {
|
||||
apiURL = defaultAPIURL
|
||||
@@ -50,6 +51,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
@@ -57,13 +59,19 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
ChatID string `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
ParseMode string `json:"parse_mode"`
|
||||
}
|
||||
|
||||
// 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(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\\n—\\n _healthcheck passing successfully %d time(s) in a row_\\n— ", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
} 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— ", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
@@ -72,15 +80,20 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
} else {
|
||||
prefix = "❌"
|
||||
}
|
||||
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
|
||||
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var text string
|
||||
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\n*Condition results*\n%s", message, alert.GetDescription(), results)
|
||||
} else {
|
||||
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Condition results*\\n%s", message, results)
|
||||
text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Condition results*\n%s", message, results)
|
||||
}
|
||||
return fmt.Sprintf(`{"chat_id": "%s", "text": "%s", "parse_mode": "MARKDOWN"}`, provider.ID, text)
|
||||
body, _ := json.Marshal(Body{
|
||||
ChatID: provider.ID,
|
||||
Text: text,
|
||||
ParseMode: "MARKDOWN",
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
||||
@@ -124,14 +124,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{ID: "123"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\"chat_id\": \"123\", \"text\": \"⛑ *Gatus* \\nAn alert for *endpoint-name* has been triggered:\\n—\\n _healthcheck failed 3 time(s) in a row_\\n— \\n*Description* \\n_description-1_ \\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 triggered:\\n—\\n _healthcheck failed 3 time(s) in a row_\\n— \\n*Description* \\n_description-1_ \\n\\n*Condition results*\\n❌ - `[CONNECTED] == true`\\n❌ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{ID: "123"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\"chat_id\": \"123\", \"text\": \"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 3 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\", \"parse_mode\": \"MARKDOWN\"}",
|
||||
ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 3 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -147,11 +147,11 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
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([]byte(body), &out); err != nil {
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,6 +42,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
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))
|
||||
|
||||
@@ -302,6 +302,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
||||
alertTypes := []alert.Type{
|
||||
alert.TypeCustom,
|
||||
alert.TypeDiscord,
|
||||
alert.TypeGoogleChat,
|
||||
alert.TypeEmail,
|
||||
alert.TypeMatrix,
|
||||
alert.TypeMattermost,
|
||||
|
||||
+45
-15
@@ -19,7 +19,23 @@ func Eval(path string, b []byte) (string, int, error) {
|
||||
}
|
||||
|
||||
func walk(path string, object interface{}) (string, int, error) {
|
||||
keys := strings.Split(path, ".")
|
||||
var keys []string
|
||||
startOfCurrentKey, bracketDepth := 0, 0
|
||||
for i := range path {
|
||||
if path[i] == '[' {
|
||||
bracketDepth++
|
||||
} else if path[i] == ']' {
|
||||
bracketDepth--
|
||||
}
|
||||
// If we encounter a dot, we've reached the end of a key unless we're inside a bracket
|
||||
if path[i] == '.' && bracketDepth == 0 {
|
||||
keys = append(keys, path[startOfCurrentKey:i])
|
||||
startOfCurrentKey = i + 1
|
||||
}
|
||||
}
|
||||
if startOfCurrentKey <= len(path) {
|
||||
keys = append(keys, path[startOfCurrentKey:])
|
||||
}
|
||||
currentKey := keys[0]
|
||||
switch value := extractValue(currentKey, object).(type) {
|
||||
case map[string]interface{}:
|
||||
@@ -41,33 +57,47 @@ func walk(path string, object interface{}) (string, int, error) {
|
||||
func extractValue(currentKey string, value interface{}) interface{} {
|
||||
// Check if the current key ends with [#]
|
||||
if strings.HasSuffix(currentKey, "]") && strings.Contains(currentKey, "[") {
|
||||
tmp := strings.SplitN(currentKey, "[", 3)
|
||||
arrayIndex, err := strconv.Atoi(strings.Replace(tmp[1], "]", "", 1))
|
||||
var isNestedArray bool
|
||||
var index string
|
||||
startOfBracket, endOfBracket, bracketDepth := 0, 0, 0
|
||||
for i := range currentKey {
|
||||
if currentKey[i] == '[' {
|
||||
startOfBracket = i
|
||||
bracketDepth++
|
||||
} else if currentKey[i] == ']' && bracketDepth == 1 {
|
||||
bracketDepth--
|
||||
endOfBracket = i
|
||||
index = currentKey[startOfBracket+1 : i]
|
||||
if len(currentKey) > i+1 && currentKey[i+1] == '[' {
|
||||
isNestedArray = true // there's more keys.
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
arrayIndex, err := strconv.Atoi(index)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
currentKey := tmp[0]
|
||||
// if currentKey contains only an index (i.e. [0] or 0)
|
||||
if len(currentKey) == 0 {
|
||||
currentKeyWithoutIndex := currentKey[:startOfBracket]
|
||||
// if currentKeyWithoutIndex contains only an index (i.e. [0] or 0)
|
||||
if len(currentKeyWithoutIndex) == 0 {
|
||||
array := value.([]interface{})
|
||||
if len(array) > arrayIndex {
|
||||
if len(tmp) > 2 {
|
||||
// Nested array? Go deeper.
|
||||
return extractValue(fmt.Sprintf("%s[%s", currentKey, tmp[2]), array[arrayIndex])
|
||||
if isNestedArray {
|
||||
return extractValue(currentKey[endOfBracket+1:], array[arrayIndex])
|
||||
}
|
||||
return array[arrayIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if value == nil || value.(map[string]interface{})[currentKey] == nil {
|
||||
if value == nil || value.(map[string]interface{})[currentKeyWithoutIndex] == nil {
|
||||
return nil
|
||||
}
|
||||
// if currentKey contains both a key and an index (i.e. data[0])
|
||||
array := value.(map[string]interface{})[currentKey].([]interface{})
|
||||
// if currentKeyWithoutIndex contains both a key and an index (i.e. data[0])
|
||||
array := value.(map[string]interface{})[currentKeyWithoutIndex].([]interface{})
|
||||
if len(array) > arrayIndex {
|
||||
if len(tmp) > 2 {
|
||||
// Nested array? Go deeper.
|
||||
return extractValue(fmt.Sprintf("[%s", tmp[2]), array[arrayIndex])
|
||||
if isNestedArray {
|
||||
return extractValue(currentKey[endOfBracket+1:], array[arrayIndex])
|
||||
}
|
||||
return array[arrayIndex]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package jsonpath
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkEval(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Eval("ids[0]", []byte(`{"ids": [1, 2]}`))
|
||||
Eval("long.simple.walk", []byte(`{"long": {"simple": {"walk": "value"}}}`))
|
||||
Eval("data[0].apps[1].name", []byte(`{"data": [{"apps": [{"name":"app1"}, {"name":"app2"}, {"name":"app3"}]}]}`))
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,22 @@ func TestEval(t *testing.T) {
|
||||
ExpectedOutputLength: 5,
|
||||
ExpectedError: false,
|
||||
},
|
||||
{
|
||||
Name: "map-of-arrays-of-maps",
|
||||
Path: "data[0].apps[1].name",
|
||||
Data: `{"data": [{"apps": [{"name":"app1"}, {"name":"app2"}, {"name":"app3"}]}]}`,
|
||||
ExpectedOutput: "app2",
|
||||
ExpectedOutputLength: 4,
|
||||
ExpectedError: false,
|
||||
},
|
||||
{
|
||||
Name: "map-of-arrays-of-maps-with-missing-element",
|
||||
Path: "data[0].apps[1].name",
|
||||
Data: `{"data": [{"apps": []}]}`,
|
||||
ExpectedOutput: "",
|
||||
ExpectedOutputLength: 0,
|
||||
ExpectedError: true,
|
||||
},
|
||||
{
|
||||
Name: "partially-invalid-path-issue122",
|
||||
Path: "data.name.invalid",
|
||||
|
||||
Generated
+33
@@ -8,6 +8,8 @@
|
||||
"name": "gatus",
|
||||
"version": "4.0.0",
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.3",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"core-js": "3.22.8",
|
||||
"vue": "3.2.37",
|
||||
"vue-router": "4.0.16"
|
||||
@@ -1768,6 +1770,25 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/vue": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.3.tgz",
|
||||
"integrity": "sha512-Is4iakKts9u9E0+jEZNzoJpBjwq2SamwEIoEl2RlyYSu6Zco536GsPXaQEfg/o7Eyc1GUUlcL+dJd4Rt7qyf7A==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@heroicons/vue": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.0.12.tgz",
|
||||
"integrity": "sha512-ypuQl/Wei7BoShO65AdRVikXDaSt6hJBRuKs2pVd/6HvxfEz18oXraH+J5G/QPIwObMEVhMb6nkOs9l3WZv/gA==",
|
||||
"peerDependencies": {
|
||||
"vue": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||
@@ -12804,6 +12825,18 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"@headlessui/vue": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.3.tgz",
|
||||
"integrity": "sha512-Is4iakKts9u9E0+jEZNzoJpBjwq2SamwEIoEl2RlyYSu6Zco536GsPXaQEfg/o7Eyc1GUUlcL+dJd4Rt7qyf7A==",
|
||||
"requires": {}
|
||||
},
|
||||
"@heroicons/vue": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.0.12.tgz",
|
||||
"integrity": "sha512-ypuQl/Wei7BoShO65AdRVikXDaSt6hJBRuKs2pVd/6HvxfEz18oXraH+J5G/QPIwObMEVhMb6nkOs9l3WZv/gA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.3",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"core-js": "3.22.8",
|
||||
"vue": "3.2.37",
|
||||
"vue-router": "4.0.16"
|
||||
|
||||
+4
-9
@@ -7,10 +7,10 @@
|
||||
<div class="text-3xl xl:text-5xl lg:text-4xl font-light">{{ header }}</div>
|
||||
</div>
|
||||
<div class="w-1/4 flex justify-end">
|
||||
<a :href="link" target="_blank" style="width:100px">
|
||||
<component :is="link ? 'a' : 'div'" :href="link" target="_blank" style="width:100px">
|
||||
<img v-if="logo" :src="logo" alt="Gatus" class="object-scale-down" style="max-width: 100px; min-width: 50px; min-height:50px;" />
|
||||
<img v-else src="./assets/logo.svg" alt="Gatus" class="object-scale-down" style="max-width: 100px; min-width: 50px; min-height:50px;" />
|
||||
</a>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="buttons" class="flex flex-wrap">
|
||||
@@ -27,12 +27,7 @@
|
||||
<h2 class="mt-4 text-center text-4xl font-extrabold text-gray-800 dark:text-gray-200">
|
||||
Gatus
|
||||
</h2>
|
||||
<div class="mt-8 py-7 px-4 rounded-sm sm:bg-gray-100 sm:border sm:border-gray-300 sm:shadow-2xl sm:px-10">
|
||||
<div class="sm:mx-auto sm:w-full">
|
||||
<h2 class="mb-3 text-center text-xl font-bold text-gray-600 dark:text-gray-200 dark:sm:text-gray-600 ">
|
||||
Sign in
|
||||
</h2>
|
||||
</div>
|
||||
<div class="py-7 px-4 rounded-sm sm:px-10">
|
||||
<div v-if="$route && $route.query.error" class="text-red-500 text-center mb-5">
|
||||
<div class="text-sm">
|
||||
<span class="text-red-500" v-if="$route.query.error === 'access_denied'">You do not have access to this status page</span>
|
||||
@@ -40,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a :href="`${SERVER_URL}/oidc/login`" class="max-w-lg mx-auto w-full flex justify-center py-3 px-4 border border-green-800 rounded-md shadow-lg text-sm text-white bg-green-700 hover:bg-green-800">
|
||||
<a :href="`${SERVER_URL}/oidc/login`" class="max-w-lg mx-auto w-full flex justify-center py-3 px-4 border border-green-800 rounded-md shadow-lg text-sm text-white bg-green-700 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800">
|
||||
Login with OIDC
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -8,7 +8,7 @@
|
||||
<span v-if="data.results && data.results.length && data.results[data.results.length - 1].hostname" class='text-gray-500 font-light'> | {{ data.results[data.results.length - 1].hostname }}</span>
|
||||
</div>
|
||||
<div class='w-1/4 text-right'>
|
||||
<span class='font-light overflow-x-hidden cursor-pointer select-none' v-if="data.results && data.results.length" @click="toggleShowAverageResponseTime" :title="showAverageResponseTime ? 'Average response time' : 'Minimum and maximum response time'">
|
||||
<span class='font-light overflow-x-hidden cursor-pointer select-none hover:text-gray-500' v-if="data.results && data.results.length" @click="toggleShowAverageResponseTime" :title="showAverageResponseTime ? 'Average response time' : 'Minimum and maximum response time'">
|
||||
<slot v-if="showAverageResponseTime">
|
||||
~{{ averageResponseTime }}ms
|
||||
</slot>
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
<div :class="endpoints.length === 0 ? 'mt-3' : 'mt-4'">
|
||||
<slot v-if="name !== 'undefined'">
|
||||
<div class="endpoint-group pt-2 border dark:bg-gray-800 dark:border-gray-500" @click="toggleGroup">
|
||||
<h5 class='font-mono text-gray-400 text-xl font-medium pb-2 px-3 dark:text-gray-200 dark:hover:text-gray-500 dark:border-gray-500'>
|
||||
<span v-if="healthy" class='text-green-600'>✓</span>
|
||||
<span v-else class='text-yellow-400'>~</span>
|
||||
{{ name }}
|
||||
<span class='float-right endpoint-group-arrow'>
|
||||
<h5 class="font-mono text-gray-400 text-xl font-medium pb-2 px-3 dark:text-gray-200 dark:hover:text-gray-500 dark:border-gray-500">
|
||||
<span class="endpoint-group-arrow mr-2">
|
||||
{{ collapsed ? '▼' : '▲' }}
|
||||
</span>
|
||||
{{ name }}
|
||||
<span v-if="healthy" class="float-right text-green-600 w-7 hover:scale-110" title="Operational">
|
||||
<CheckCircleIcon />
|
||||
</span>
|
||||
<span v-else class="float-right text-yellow-500 text-sm w-7 hover:scale-110" title="Partial Outage">
|
||||
<ExclamationCircleIcon />
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</slot>
|
||||
@@ -28,11 +32,14 @@
|
||||
|
||||
<script>
|
||||
import Endpoint from './Endpoint.vue';
|
||||
import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/vue/20/solid'
|
||||
|
||||
export default {
|
||||
name: 'EndpointGroup',
|
||||
components: {
|
||||
Endpoint
|
||||
Endpoint,
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
@@ -44,9 +51,8 @@ export default {
|
||||
healthCheck() {
|
||||
if (this.endpoints) {
|
||||
for (let i in this.endpoints) {
|
||||
for (let j in this.endpoints[i].results) {
|
||||
if (!this.endpoints[i].results[j].success) {
|
||||
// Set the endpoint group to unhealthy (only if it's currently healthy)
|
||||
if (this.endpoints[i].results && this.endpoints[i].results.length > 0) {
|
||||
if (!this.endpoints[i].results[this.endpoints[i].results.length-1].success) {
|
||||
if (this.healthy) {
|
||||
this.healthy = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="settings" class="flex bg-gray-200 border-gray-300 rounded border shadow dark:text-gray-200 dark:bg-gray-800 dark:border-gray-500">
|
||||
<div class="text-xs text-gray-600 rounded-xl py-1 px-2 dark:text-gray-200">
|
||||
↻
|
||||
<div class="text-xs text-gray-600 rounded-xl py-1.5 px-1.5 dark:text-gray-200">
|
||||
<ArrowPathIcon class="w-3"/>
|
||||
</div>
|
||||
<select class="text-center text-gray-500 text-xs dark:text-gray-200 dark:bg-gray-800 border-r border-l border-gray-300 dark:border-gray-500" id="refresh-rate" ref="refreshInterval" @change="handleChangeRefreshInterval">
|
||||
<option value="10" :selected="refreshInterval === 10">10s</option>
|
||||
@@ -12,16 +12,24 @@
|
||||
<option value="600" :selected="refreshInterval === 600">10m</option>
|
||||
</select>
|
||||
<button @click="toggleDarkMode" class="text-xs p-1">
|
||||
<slot v-if="darkMode">☀</slot>
|
||||
<slot v-else>🌙</slot>
|
||||
<slot v-if="darkMode"><SunIcon class="w-4"/></slot>
|
||||
<slot v-else><MoonIcon class="w-4 text-gray-500"/></slot>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { MoonIcon, SunIcon } from '@heroicons/vue/20/solid'
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
components: {
|
||||
ArrowPathIcon,
|
||||
MoonIcon,
|
||||
SunIcon
|
||||
},
|
||||
props: {},
|
||||
methods: {
|
||||
setRefreshInterval(seconds) {
|
||||
|
||||
@@ -67,17 +67,14 @@
|
||||
<hr />
|
||||
<ul role="list" class="px-0 xl:px-24 divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<li v-for="event in events" :key="event" class="p-3 my-4">
|
||||
<h2 class="text-lg">
|
||||
<img v-if="event.type === 'HEALTHY'" src="../assets/arrow-up-green.png" alt="Healthy"
|
||||
class="border border-green-600 rounded-full opacity-75 bg-green-100 mr-2 inline" width="26"/>
|
||||
<img v-else-if="event.type === 'UNHEALTHY'" src="../assets/arrow-down-red.png" alt="Unhealthy"
|
||||
class="border border-red-500 rounded-full opacity-75 bg-red-100 mr-2 inline" width="26"/>
|
||||
<img v-else-if="event.type === 'START'" src="../assets/arrow-right-black.png" alt="Start"
|
||||
class="border border-gray-500 rounded-full opacity-75 bg-gray-100 mr-2 inline" width="26"/>
|
||||
<h2 class="text-sm sm:text-lg">
|
||||
<ArrowUpCircleIcon v-if="event.type === 'HEALTHY'" class="w-8 inline mr-2 text-green-600" />
|
||||
<ArrowDownCircleIcon v-else-if="event.type === 'UNHEALTHY'" class="w-8 inline mr-2 text-red-500" />
|
||||
<PlayCircleIcon v-else-if="event.type === 'START'" class="w-8 inline mr-2 text-gray-400 dark:text-gray-100" />
|
||||
{{ event.fancyText }}
|
||||
</h2>
|
||||
<div class="flex mt-1 text-sm text-gray-400">
|
||||
<div class="flex-1 text-left pl-10">
|
||||
<div class="flex mt-1 text-xs sm:text-sm text-gray-400">
|
||||
<div class="flex-2 text-left pl-12">
|
||||
{{ prettifyTimestamp(event.timestamp) }}
|
||||
</div>
|
||||
<div class="flex-1 text-right">
|
||||
@@ -98,6 +95,7 @@ import Endpoint from '@/components/Endpoint.vue';
|
||||
import {SERVER_URL} from "@/main.js";
|
||||
import {helper} from "@/mixins/helper.js";
|
||||
import Pagination from "@/components/Pagination";
|
||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon, PlayCircleIcon } from '@heroicons/vue/20/solid'
|
||||
|
||||
export default {
|
||||
name: 'Details',
|
||||
@@ -105,6 +103,9 @@ export default {
|
||||
Pagination,
|
||||
Endpoint,
|
||||
Settings,
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
PlayCircleIcon
|
||||
},
|
||||
emits: ['showTooltip'],
|
||||
mixins: [helper],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user