Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab2bee9c4b | ||
|
|
d1ced94030 | ||
|
|
a3e35c862c | ||
|
|
0193a200b8 | ||
|
|
7224464202 | ||
|
|
c457aadcab | ||
|
|
f38b12d55b | ||
|
|
e4c9ad8796 | ||
|
|
5be1465b13 | ||
|
|
7215aa4bd6 | ||
|
|
829a9c2679 | ||
|
|
dfcdc57a18 | ||
|
|
43e8c57701 | ||
|
|
076f5c45e8 | ||
|
|
6d3c3d0892 | ||
|
|
e620fd1214 | ||
|
|
5807d76c2f | ||
|
|
017847240d | ||
|
|
c873b0ba0c | ||
|
|
6f3150d936 | ||
|
|
0792f5490b | ||
|
|
326ea1c3d1 | ||
|
|
fea95b8479 | ||
|
|
6d64c3c250 | ||
|
|
2b9d3e99d3 | ||
|
|
9a5f245440 | ||
|
|
793172c783 | ||
|
|
9f343bacf7 | ||
|
|
c31cb7540d | ||
|
|
f9efa28223 | ||
|
|
2cbb35fe3b | ||
|
|
f23fcbedb8 | ||
|
|
ad10f975b4 | ||
|
|
1c03524ca8 | ||
|
|
4af135d1fb | ||
|
|
93b5a867bb | ||
|
|
f899f41d16 | ||
|
|
ab52676f23 | ||
|
|
27fc784411 | ||
|
|
d929c09c56 | ||
|
|
cff06e38cb | ||
|
|
5b1aeaeb0c | ||
|
|
90e9b55109 | ||
|
|
cf9c00a2ad | ||
|
|
fbdb5a3f0f | ||
|
|
dde930bed7 | ||
|
|
a9fc876173 | ||
|
|
08b31ba263 | ||
|
|
9ede992e4e | ||
|
|
dcb997f501 | ||
|
|
c8efdac23a | ||
|
|
e307d1ab35 | ||
|
|
e6c6b4e06f | ||
|
|
5843c58a36 | ||
|
|
5281f8068d | ||
|
|
86d5dabf90 | ||
|
|
a81c81e42c | ||
|
|
bec2820969 | ||
|
|
0bf2271a73 | ||
|
|
bd4b91bbbd | ||
|
|
fdec317df0 | ||
|
|
8970ad5ad5 | ||
|
|
c4255e65bc | ||
|
|
fcf046cbe8 | ||
|
|
6932edc6d0 | ||
|
|
3f961a7408 | ||
|
|
4d0f3b6997 | ||
|
|
5a06599d96 | ||
|
|
d2a73a3590 | ||
|
|
932ecc436a | ||
|
|
1613274cb0 | ||
|
|
0b4720d94b | ||
|
|
16df341581 | ||
|
|
a848776a34 | ||
|
|
681b1c63f1 | ||
|
|
51a4b63fb5 | ||
|
|
3a7977d086 | ||
|
|
c682520dd9 | ||
|
|
24b7258338 | ||
|
|
89e6e4abd8 | ||
|
|
4700f54798 | ||
|
|
9ca4442e6a | ||
|
|
ce6f58f403 |
@@ -1,23 +1,41 @@
|
||||
## Usage
|
||||
Gatus exposes Prometheus metrics at `/metrics` if the `metrics` configuration option is set to `true`.
|
||||
|
||||
To run this example, all you need to do is execute the following command:
|
||||
```console
|
||||
docker-compose up
|
||||
```
|
||||
Once you've done the above, you should be able to access the Grafana dashboard at `http://localhost:3000`.
|
||||
|
||||
## Queries
|
||||
Gatus uses Prometheus counters.
|
||||

|
||||
|
||||
Total results per minute:
|
||||
|
||||
## Queries
|
||||
By default, this example has a Grafana dashboard with some panels, but for the sake of verbosity, you'll find
|
||||
a list of simple queries below. Those make use of the `key` parameter, which is a concatenation of the endpoint's
|
||||
group and name.
|
||||
|
||||
### Success rate
|
||||
```
|
||||
sum(rate(gatus_results_total{success="true"}[30s])) by (key) / sum(rate(gatus_results_total[30s])) by (key)
|
||||
```
|
||||
|
||||
### Response time
|
||||
```
|
||||
gatus_results_duration_seconds
|
||||
```
|
||||
|
||||
### Total results per minute
|
||||
```
|
||||
sum(rate(gatus_results_total[5m])*60) by (key)
|
||||
```
|
||||
|
||||
Total successful results per minute:
|
||||
### Total successful results per minute
|
||||
```
|
||||
sum(rate(gatus_results_total{success="true"}[5m])*60) by (key)
|
||||
```
|
||||
|
||||
Total unsuccessful results per minute:
|
||||
### Total unsuccessful results per minute
|
||||
```
|
||||
sum(rate(gatus_results_total{success="false"}[5m])*60) by (key)
|
||||
```
|
||||
sum(rate(gatus_results_total{success="true"}[5m])*60) by (key)
|
||||
```
|
||||
@@ -2,15 +2,18 @@ metrics: true
|
||||
endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/health
|
||||
interval: 30s
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: example
|
||||
url: https://example.com/
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: github
|
||||
url: https://api.github.com/healthz
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- name: example
|
||||
url: https://example.com/
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
- ./config:/config
|
||||
networks:
|
||||
- metrics
|
||||
|
||||
|
||||
@@ -15,8 +15,266 @@
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 3,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"datasource": null,
|
||||
"description": "Number of successful results compared to the total number of results during the current interval",
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 9,
|
||||
"links": [],
|
||||
"options": {
|
||||
"fieldOptions": {
|
||||
"calcs": [
|
||||
"mean"
|
||||
],
|
||||
"defaults": {
|
||||
"mappings": [
|
||||
{
|
||||
"id": 0,
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"type": 1,
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"max": 1,
|
||||
"min": 0,
|
||||
"nullValueMode": "connected",
|
||||
"thresholds": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "semi-dark-orange",
|
||||
"value": 0.6
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.8
|
||||
},
|
||||
{
|
||||
"color": "dark-green",
|
||||
"value": 0.95
|
||||
}
|
||||
],
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"override": {},
|
||||
"values": false
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": false
|
||||
},
|
||||
"pluginVersion": "6.4.4",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(gatus_results_total{success=\"true\"}[30s])) by (key) / sum(rate(gatus_results_total[30s])) by (key)",
|
||||
"hide": false,
|
||||
"legendFormat": "{{key}}",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Success rate",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"cacheTimeout": null,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 11,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null as zero",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "6.4.4",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "gatus_results_duration_seconds",
|
||||
"format": "time_series",
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{key}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Response time",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"cacheTimeout": null,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": null,
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 10,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "6.4.4",
|
||||
"pointradius": 2,
|
||||
"points": true,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(gatus_results_total{success=\"true\"}[30s])) by (key) / sum(rate(gatus_results_total[30s])) by (key)",
|
||||
"format": "time_series",
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{key}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Success rate",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
@@ -27,10 +285,10 @@
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"id": 2,
|
||||
"interval": "",
|
||||
@@ -126,8 +384,8 @@
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 5,
|
||||
"legend": {
|
||||
@@ -204,94 +462,6 @@
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorPostfix": false,
|
||||
"colorPrefix": false,
|
||||
"colorValue": true,
|
||||
"colors": [
|
||||
"#299c46",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#d44a3a"
|
||||
],
|
||||
"datasource": null,
|
||||
"format": "none",
|
||||
"gauge": {
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"show": false,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 7,
|
||||
"interval": "",
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"options": {},
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": false,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": true,
|
||||
"ymax": null,
|
||||
"ymin": null
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(gatus_results_total{success=\"false\"}[1m])*60",
|
||||
"format": "time_series",
|
||||
"instant": false,
|
||||
"intervalFactor": 1,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": "1,2",
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Unsuccessful results",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "150%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "current"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
@@ -304,7 +474,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 7
|
||||
"y": 16
|
||||
},
|
||||
"id": 3,
|
||||
"legend": {
|
||||
@@ -380,7 +550,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": "10s",
|
||||
"refresh": "1m",
|
||||
"schemaVersion": 20,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
@@ -391,9 +561,22 @@
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Gatus",
|
||||
"uid": "KPI7Qj1Wk",
|
||||
"version": 1
|
||||
"version": 2
|
||||
}
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
- ./config:/config
|
||||
networks:
|
||||
- default
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ endpoints:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: example-dns-query
|
||||
url: "8.8.8.8" # Address of the DNS server to use
|
||||
url: "1.1.1.1" # Address of the DNS server to use
|
||||
interval: 5m
|
||||
dns:
|
||||
query-name: "example.com"
|
||||
@@ -19,11 +19,11 @@ services:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
- ./config:/config
|
||||
networks:
|
||||
- web
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
networks:
|
||||
web:
|
||||
web:
|
||||
|
||||
@@ -26,7 +26,7 @@ endpoints:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: example-dns-query
|
||||
url: "8.8.8.8" # Address of the DNS server to use
|
||||
url: "1.1.1.1" # Address of the DNS server to use
|
||||
interval: 5m
|
||||
dns:
|
||||
query-name: "example.com"
|
||||
@@ -5,5 +5,5 @@ services:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
- ./data:/data/
|
||||
- ./config:/config
|
||||
- ./data:/data/
|
||||
|
||||
@@ -5,4 +5,4 @@ services:
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml
|
||||
- ./config:/config
|
||||
|
||||
@@ -54,10 +54,10 @@ spec:
|
||||
app: gatus
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gatus
|
||||
name: gatus
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app: gatus
|
||||
spec:
|
||||
serviceAccountName: gatus
|
||||
terminationGracePeriodSeconds: 5
|
||||
@@ -76,6 +76,22 @@ spec:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 30M
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 5
|
||||
volumeMounts:
|
||||
- mountPath: /config
|
||||
name: gatus-config
|
||||
|
||||
BIN
.github/assets/grafana-dashboard.png
vendored
Normal file
BIN
.github/assets/grafana-dashboard.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
26
.github/workflows/benchmark.yml
vendored
Normal file
26
.github/workflows/benchmark.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: benchmark
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
repository:
|
||||
description: "Repository to checkout. Useful for benchmarking a fork. Format should be <owner>/<repository>."
|
||||
required: true
|
||||
default: "TwiN/gatus"
|
||||
ref:
|
||||
description: "Branch, tag or SHA to checkout"
|
||||
required: true
|
||||
default: "master"
|
||||
jobs:
|
||||
build:
|
||||
name: benchmark
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
repository: "${{ github.event.inputs.repository }}"
|
||||
ref: "${{ github.event.inputs.ref }}"
|
||||
- uses: actions/checkout@v3
|
||||
- name: Benchmark
|
||||
run: go test -bench=. ./storage/store
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -10,16 +10,14 @@ on:
|
||||
- '*.md'
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.17
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
go-version: 1.18
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build binary to make sure it works
|
||||
run: go build -mod vendor
|
||||
- name: Test
|
||||
|
||||
9
.github/workflows/publish-latest.yml
vendored
9
.github/workflows/publish-latest.yml
vendored
@@ -6,13 +6,12 @@ on:
|
||||
types: [completed]
|
||||
jobs:
|
||||
publish-latest:
|
||||
name: Publish latest
|
||||
name: publish-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Set up QEMU
|
||||
@@ -27,7 +26,7 @@ jobs:
|
||||
- name: Build and push docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
pull: true
|
||||
push: true
|
||||
tags: |
|
||||
|
||||
7
.github/workflows/publish-release.yml
vendored
7
.github/workflows/publish-release.yml
vendored
@@ -4,12 +4,11 @@ on:
|
||||
types: [published]
|
||||
jobs:
|
||||
publish-release:
|
||||
name: Publish release
|
||||
name: publish-release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Get the release
|
||||
|
||||
5
Makefile
5
Makefile
@@ -1,5 +1,8 @@
|
||||
BINARY=gatus
|
||||
|
||||
# Because there's a folder called "test", we need to make the target "test" phony
|
||||
.PHONY: test
|
||||
|
||||
install:
|
||||
go build -mod vendor -o $(BINARY) .
|
||||
|
||||
@@ -10,7 +13,7 @@ clean:
|
||||
rm $(BINARY)
|
||||
|
||||
test:
|
||||
sudo go test ./alerting/... ./client/... ./config/... ./controller/... ./core/... ./jsonpath/... ./pattern/... ./security/... ./storage/... ./util/... ./watchdog/... -cover
|
||||
go test ./... -cover
|
||||
|
||||
|
||||
##########
|
||||
|
||||
427
README.md
427
README.md
@@ -7,7 +7,7 @@
|
||||
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
||||
[](https://github.com/TwiN)
|
||||
|
||||
Gatus is a 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,
|
||||
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
||||
checks can be paired with alerting via Slack, PagerDuty, Discord, Twilio and more.
|
||||
@@ -41,8 +41,10 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
||||
- [Alerting](#alerting)
|
||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||
- [Configuring Email alerts](#configuring-email-alerts)
|
||||
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
||||
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
||||
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
||||
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
||||
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
||||
- [Configuring Slack alerts](#configuring-slack-alerts)
|
||||
- [Configuring Teams alerts](#configuring-teams-alerts)
|
||||
@@ -54,6 +56,7 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
||||
- [Security](#security)
|
||||
- [Basic](#basic)
|
||||
- [OIDC (ALPHA)](#oidc-alpha)
|
||||
- [Metrics](#metrics)
|
||||
- [Deployment](#deployment)
|
||||
- [Docker](#docker)
|
||||
- [Helm Chart](#helm-chart)
|
||||
@@ -75,6 +78,7 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||
- [Badges](#badges)
|
||||
- [Uptime](#uptime)
|
||||
- [Health](#health)
|
||||
- [Response time](#response-time)
|
||||
- [API](#api)
|
||||
- [High level design overview](#high-level-design-overview)
|
||||
@@ -93,7 +97,7 @@ monitor these features and potentially alert you before any clients are impacted
|
||||
|
||||
A sign you may want to look into Gatus is by simply asking yourself whether you'd receive an alert if your load balancer
|
||||
was to go down right now. Will any of your existing alerts be triggered? Your metrics won’t report an increase in errors
|
||||
if there’s no traffic that makes it to your applications. This puts you in a situation where your clients are the ones
|
||||
if no traffic makes it to your applications. This puts you in a situation where your clients are the ones
|
||||
that will notify you about the degradation of your services rather than you reassuring them that you're working on
|
||||
fixing the issue before they even know about it.
|
||||
|
||||
@@ -103,7 +107,7 @@ 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.
|
||||
- **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.
|
||||
- **Alerting**: While having a pretty visual dashboard is useful to keep track of the state of your application(s), you probably don't want to stare at it all day. Thus, notifications via Slack, Mattermost, Messagebird, PagerDuty, Twilio and Teams are supported out of the box with the ability to configure a custom alerting provider for any needs you might have, whether it be a different provider or a custom application that manages automated rollbacks.
|
||||
- **Alerting**: While having a pretty visual dashboard is useful to keep track of the state of your application(s), you probably don't want to stare at it all day. Thus, notifications via Slack, Mattermost, Messagebird, PagerDuty, Twilio, Google chat and Teams are supported out of the box with the ability to configure a custom alerting provider for any needs you might have, whether it be a different provider or a custom application that manages automated rollbacks.
|
||||
- **Metrics**
|
||||
- **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small.
|
||||
- **[Badges](#badges)**:  
|
||||
@@ -143,48 +147,52 @@ If you want to test it locally, see [Docker](#docker).
|
||||
|
||||
|
||||
## Configuration
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
||||
| `debug` | Whether to enable debug logs. | `false` |
|
||||
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
||||
| `storage` | [Storage configuration](#storage) | `{}` |
|
||||
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
||||
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
||||
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
||||
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
||||
| `endpoints[].url` | URL to send the request to. | Required `""` |
|
||||
| `endpoints[].method` | Request method. | `GET` |
|
||||
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
||||
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
|
||||
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
||||
| `endpoints[].body` | Request body. | `""` |
|
||||
| `endpoints[].headers` | Request headers. | `{}` |
|
||||
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
||||
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
|
||||
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
||||
| `endpoints[].alerts[].type` | Type of alert. <br />Valid types: `slack`, `discord`, `email`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
|
||||
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `false` |
|
||||
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
||||
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
||||
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
||||
| `endpoints[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
||||
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
||||
| `endpoints[].ui.hide-hostname` | Whether to include the hostname in the result. | `false` |
|
||||
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
||||
| `security` | [Security configuration](#security). | `{}` |
|
||||
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
||||
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
|
||||
| `web` | Web configuration. | `{}` |
|
||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
||||
| `web.port` | Port to listen on. | `8080` |
|
||||
| `ui` | UI configuration. | `{}` |
|
||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
||||
| `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. | `""` |
|
||||
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
||||
| `debug` | Whether to enable debug logs. | `false` |
|
||||
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
||||
| `storage` | [Storage configuration](#storage) | `{}` |
|
||||
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
||||
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
||||
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
||||
| `endpoints[].group` | Group name. Used to group multiple endpoints together on the dashboard. <br />See [Endpoint groups](#endpoint-groups). | `""` |
|
||||
| `endpoints[].url` | URL to send the request to. | Required `""` |
|
||||
| `endpoints[].method` | Request method. | `GET` |
|
||||
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
||||
| `endpoints[].interval` | Duration to wait between every status check. | `60s` |
|
||||
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
||||
| `endpoints[].body` | Request body. | `""` |
|
||||
| `endpoints[].headers` | Request headers. | `{}` |
|
||||
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
||||
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
|
||||
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
||||
| `endpoints[].alerts[].type` | Type of alert. <br />Valid types: `slack`, `discord`, `email`, `googlechat`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
|
||||
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `false` |
|
||||
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
||||
| `endpoints[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
|
||||
| `endpoints[].alerts[].send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved. | `false` |
|
||||
| `endpoints[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
||||
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
||||
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
|
||||
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
|
||||
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
||||
| `security` | [Security configuration](#security). | `{}` |
|
||||
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
||||
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
|
||||
| `web` | Web configuration. | `{}` |
|
||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
||||
| `web.port` | Port to listen on. | `8080` |
|
||||
| `ui` | UI configuration. | `{}` |
|
||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
||||
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
|
||||
| `ui.logo` | URL to the logo to display. | `""` |
|
||||
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
||||
| `ui.buttons` | List of buttons to display below the header. | `[]` |
|
||||
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
|
||||
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
|
||||
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
|
||||
|
||||
|
||||
### Conditions
|
||||
@@ -214,15 +222,15 @@ Here are some examples of conditions you can use:
|
||||
|
||||
|
||||
#### Placeholders
|
||||
| Placeholder | Description | Example of resolved value |
|
||||
|:---------------------------|:---------------------------------------------------------|:---------------------------------------------|
|
||||
| `[STATUS]` | Resolves into the HTTP status of the request | 404 |
|
||||
| `[RESPONSE_TIME]` | Resolves into the response time the request took, in ms | 10 |
|
||||
| `[IP]` | Resolves into the IP of the target host | 192.168.0.232 |
|
||||
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}` |
|
||||
| `[CONNECTED]` | Resolves into whether a connection could be established | `true` |
|
||||
| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration | `24h`, `48h`, 0 (if not protocol with certs) |
|
||||
| `[DNS_RCODE]` | Resolves into the DNS status of the response | NOERROR |
|
||||
| Placeholder | Description | Example of resolved value |
|
||||
|:---------------------------|:------------------------------------------------------------------------------------------|:---------------------------------------------|
|
||||
| `[STATUS]` | Resolves into the HTTP status of the request | 404 |
|
||||
| `[RESPONSE_TIME]` | Resolves into the response time the request took, in ms | 10 |
|
||||
| `[IP]` | Resolves into the IP of the target host | 192.168.0.232 |
|
||||
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}` |
|
||||
| `[CONNECTED]` | Resolves into whether a connection could be established | `true` |
|
||||
| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration (valid units are "s", "m", "h".) | `24h`, `48h`, 0 (if not protocol with certs) |
|
||||
| `[DNS_RCODE]` | Resolves into the DNS status of the response | NOERROR |
|
||||
|
||||
|
||||
#### Functions
|
||||
@@ -271,11 +279,17 @@ 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
|
||||
the client used to send the request.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------|:------------------------------------------------------------------------|:--------|
|
||||
| `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.timeout` | Duration before timing out. | `10s` |
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------|:---------------------------------------------------------------------------|:----------------|
|
||||
| `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.timeout` | Duration before timing out. | `10s` |
|
||||
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
|
||||
| `client.oauth2` | OAuth2 client configuration. | `{}` |
|
||||
| `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-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 `[""]` |
|
||||
|
||||
Note that 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.
|
||||
@@ -302,6 +316,31 @@ endpoints:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
This example shows how you can specify a custom DNS resolver:
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: with-custom-dns-resolver
|
||||
url: "https://your.health.api/health"
|
||||
client:
|
||||
dns-resolver: "tcp://1.1.1.1:53"
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
This example shows how you can use the `client.oauth2` configuration to query a backend API with `Bearer token`:
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: with-custom-oauth2
|
||||
url: "https://your.health.api/health"
|
||||
client:
|
||||
oauth2:
|
||||
token-url: https://your-token-server/token
|
||||
client-id: 00000000-0000-0000-0000-000000000000
|
||||
client-secret: your-client-secret
|
||||
scopes: ['https://your.health.api/.default']
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
### Alerting
|
||||
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
||||
@@ -314,6 +353,7 @@ ignored.
|
||||
|:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------|:--------|
|
||||
| `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.googlechat` | Configuration for alerts of type `googlechat`. <br />See [Configuring Google Chat alerts](#configuring-google-chat-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.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
||||
@@ -324,13 +364,16 @@ ignored.
|
||||
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
|
||||
| `alerting.custom` | Configuration for custom actions on failure or alerts. <br />See [Configuring Custom alerts](#configuring-custom-alerts). | `{}` |
|
||||
|
||||
|
||||
#### Configuring Discord alerts
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
||||
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
|
||||
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
||||
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
|
||||
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.discord.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.discord.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.discord.overrides[].webhook-url` | Discord Webhook URL | `""` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
@@ -340,7 +383,7 @@ alerting:
|
||||
endpoints:
|
||||
- name: website
|
||||
url: "https://twin.sh/health"
|
||||
interval: 30s
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
@@ -352,26 +395,36 @@ endpoints:
|
||||
send-on-resolved: true
|
||||
```
|
||||
|
||||
|
||||
#### Configuring Email alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
||||
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
||||
| `alerting.email.password` | Password of the email used to send the alert | Required `""` |
|
||||
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
||||
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
||||
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
|
||||
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
||||
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
||||
| `alerting.email.username` | Username of the SMTP server used to send the alert. If empty, uses `alerting.email.from`. | `""` |
|
||||
| `alerting.email.password` | Password of the SMTP server used to send the alert | Required `""` |
|
||||
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
||||
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
||||
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
|
||||
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.email.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.email.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.email.overrides[].to` | Email(s) to send the alerts to | `""` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
email:
|
||||
from: "from@example.com"
|
||||
username: "from@example.com"
|
||||
password: "hunter2"
|
||||
host: "mail.example.com"
|
||||
port: 587
|
||||
to: "recipient1@example.com,recipient2@example.com"
|
||||
# You can also add group-specific to keys, which will
|
||||
# override the to key above for the specified groups
|
||||
overrides:
|
||||
- group: "core"
|
||||
to: "recipient3@example.com,recipient4@example.com"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
@@ -386,18 +439,65 @@ endpoints:
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
|
||||
- name: back-end
|
||||
group: core
|
||||
url: "https://example.org/"
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[CERTIFICATE_EXPIRATION] > 48h"
|
||||
alerts:
|
||||
- type: email
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
```
|
||||
|
||||
**NOTE:** Some mail servers are painfully slow.
|
||||
|
||||
#### Configuring Google Chat alerts
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.googlechat` | Configuration for alerts of type `googlechat` | `{}` |
|
||||
| `alerting.googlechat.webhook-url` | Google Chat Webhook URL | Required `""` |
|
||||
| `alerting.googlechat.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
||||
| `alerting.googlechat.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||
| `alerting.googlechat.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.googlechat.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.googlechat.overrides[].webhook-url` | Teams Webhook URL | `""` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
googlechat:
|
||||
webhook-url: "https://chat.googleapis.com/v1/spaces/*******/messages?key=**********&token=********"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
url: "https://twin.sh/health"
|
||||
interval: 30s
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
- "[RESPONSE_TIME] < 300"
|
||||
alerts:
|
||||
- type: googlechat
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
```
|
||||
|
||||
#### Configuring Mattermost alerts
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
|
||||
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
|
||||
| `alerting.mattermost.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
||||
| `alerting.mattermost.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
|
||||
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
|
||||
| `alerting.mattermost.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
||||
| `alerting.mattermost.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||
| `alerting.mattermost.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.mattermost.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.mattermist.overrides[].webhook-url` | Mattermost Webhook URL | `""` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
@@ -425,7 +525,6 @@ Here's an example of what the notifications look like:
|
||||
|
||||

|
||||
|
||||
|
||||
#### Configuring Messagebird alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
@@ -459,16 +558,18 @@ endpoints:
|
||||
description: "healthcheck failed"
|
||||
```
|
||||
|
||||
|
||||
#### Configuring Opsgenie alerts
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------|:--------------------------------------------|:---------------------|
|
||||
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie` | `{}` |
|
||||
| `alerting.opsgenie.api-key` | Opsgenie API Key | Required `""` |
|
||||
| `alerting.opsgenie.priority` | Priority level of the alert. | `P1` |
|
||||
| `alerting.opsgenie.source` | Source field of the alert. | `gatus` |
|
||||
| `alerting.opsgenie.entity-prefix` | Entity field prefix. | `gatus-` |
|
||||
| `alerting.opsgenie.alias-prefix` | Alias field prefix. | `gatus-healthcheck-` |
|
||||
| `alerting.opsgenie.tags` | Tags of alert. | `[]` |
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------|:-------------------------------------------------------------------------------------------|:---------------------|
|
||||
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie` | `{}` |
|
||||
| `alerting.opsgenie.api-key` | Opsgenie API Key | Required `""` |
|
||||
| `alerting.opsgenie.priority` | Priority level of the alert. | `P1` |
|
||||
| `alerting.opsgenie.source` | Source field of the alert. | `gatus` |
|
||||
| `alerting.opsgenie.entity-prefix` | Entity field prefix. | `gatus-` |
|
||||
| `alerting.opsgenie.alias-prefix` | Alias field prefix. | `gatus-healthcheck-` |
|
||||
| `alerting.opsgenie.tags` | Tags of alert. | `[]` |
|
||||
| `alerting.opsgenie.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
Opsgenie provider will automatically open and close alerts.
|
||||
|
||||
@@ -478,26 +579,26 @@ alerting:
|
||||
api-key: "00000000-0000-0000-0000-000000000000"
|
||||
```
|
||||
|
||||
|
||||
#### Configuring PagerDuty alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------------------------|:-------------------------------------------------------------------------------------------|:--------|
|
||||
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty` | `{}` |
|
||||
| `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key | `""` |
|
||||
| `alerting.pagerduty.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.pagerduty.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.pagerduty.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.pagerduty.overrides[].integration-key` | PagerDuty Events API v2 integration key | `""` |
|
||||
| `alerting.pagerduty.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
It is highly recommended to set `endpoints[].alerts[].send-on-resolved` to `true` for alerts
|
||||
of type `pagerduty`, because unlike other alerts, the operation resulting from setting said
|
||||
parameter to `true` will not create another incident, but mark the incident as resolved on
|
||||
parameter to `true` will not create another incident but mark the incident as resolved on
|
||||
PagerDuty instead.
|
||||
|
||||
Behavior:
|
||||
- By default, `alerting.pagerduty.integration-key` is used as the integration key
|
||||
- If the endpoint being evaluated belongs to a group (`endpoints[].group`) matching the value of `alerting.pagerduty.overrides[].group`, the provider will use that override's integration key instead of `alerting.pagerduty.integration-key`'s
|
||||
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
pagerduty:
|
||||
@@ -505,8 +606,8 @@ alerting:
|
||||
# You can also add group-specific integration keys, which will
|
||||
# override the integration key above for the specified groups
|
||||
overrides:
|
||||
- group: "core"
|
||||
integration-key: "********************************"
|
||||
- group: "core"
|
||||
integration-key: "********************************"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
@@ -542,12 +643,14 @@ endpoints:
|
||||
|
||||
|
||||
#### Configuring Slack alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
|
||||
| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` |
|
||||
| `alerting.slack.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
|
||||
| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` |
|
||||
| `alerting.slack.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.slack.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.slack.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.slack.overrides[].webhook-url` | Slack Webhook URL | `""` |
|
||||
```yaml
|
||||
alerting:
|
||||
slack:
|
||||
@@ -579,16 +682,25 @@ Here's an example of what the notifications look like:
|
||||
|
||||
|
||||
#### Configuring Teams alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
||||
| `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 |
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
||||
| `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.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.overrides[].webhook-url` | Teams Webhook URL | `""` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
teams:
|
||||
webhook-url: "https://********.webhook.office.com/webhookb2/************"
|
||||
# You can also add group-specific to keys, which will
|
||||
# override the to key above for the specified groups
|
||||
overrides:
|
||||
- group: "core"
|
||||
webhook-url: "https://********.webhook.office.com/webhookb3/************"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
@@ -603,6 +715,19 @@ endpoints:
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
|
||||
- name: back-end
|
||||
group: core
|
||||
url: "https://example.org/"
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[CERTIFICATE_EXPIRATION] > 48h"
|
||||
alerts:
|
||||
- type: teams
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
```
|
||||
|
||||
Here's an example of what the notifications look like:
|
||||
@@ -610,12 +735,13 @@ Here's an example of what the notifications look like:
|
||||

|
||||
|
||||
#### Configuring Telegram alerts
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` |
|
||||
| `alerting.telegram.token` | Telegram Bot Token | Required `""` |
|
||||
| `alerting.telegram.id` | Telegram User ID | Required `""` |
|
||||
| `alerting.telegram.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------|:-------------------------------------------------------------------------------------------|:---------------------------|
|
||||
| `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` |
|
||||
| `alerting.telegram.token` | Telegram Bot Token | Required `""` |
|
||||
| `alerting.telegram.id` | Telegram User ID | Required `""` |
|
||||
| `alerting.telegram.api-url` | Telegram API URL | `https://api.telegram.org` |
|
||||
| `alerting.telegram.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
@@ -694,8 +820,11 @@ leveraging Gatus, you could have Gatus call that application endpoint when an en
|
||||
would then check if the endpoint that started failing was part of the recently deployed application, and if it was,
|
||||
then automatically roll it back.
|
||||
|
||||
The placeholders `[ALERT_DESCRIPTION]` and `[ENDPOINT_NAME]` are automatically substituted for the alert description and
|
||||
the endpoint name. These placeholders can be used in the body (`alerting.custom.body`) and in the url (`alerting.custom.url`).
|
||||
Furthermore, you may use the following placeholders in the body (`alerting.custom.body`) and in the url (`alerting.custom.url`):
|
||||
- `[ALERT_DESCRIPTION]` (resolved from `endpoints[].alerts[].description`)
|
||||
- `[ENDPOINT_NAME]` (resolved from `endpoints[].name`)
|
||||
- `[ENDPOINT_GROUP]` (resolved from `endpoints[].group`)
|
||||
- `[ENDPOINT_URL]` (resolved from `endpoints[].url`)
|
||||
|
||||
If you have an alert using the `custom` provider with `send-on-resolved` set to `true`, you can use the
|
||||
`[ALERT_TRIGGERED_OR_RESOLVED]` placeholder to differentiate the notifications.
|
||||
@@ -710,7 +839,7 @@ alerting:
|
||||
method: "POST"
|
||||
body: |
|
||||
{
|
||||
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
||||
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_GROUP] - [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
||||
}
|
||||
endpoints:
|
||||
- name: website
|
||||
@@ -915,6 +1044,21 @@ security:
|
||||
**NOTE:** The OIDC feature is currently in Alpha. Breaking changes may occur. Use this feature at your own risk.
|
||||
|
||||
|
||||
### 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`).
|
||||
|
||||
| Metric name | Type | Description | Labels | Relevant endpoint types |
|
||||
|:---------------------------------------------|:--------|:---------------------------------------------------------------------------|:--------------------------------|:------------------------|
|
||||
| gatus_results_total | counter | Number of results per endpoint | key, group, name, type, success | All |
|
||||
| gatus_results_code_total | counter | Total number of results by code | key, group, name, type, code | DNS, HTTP |
|
||||
| gatus_results_connected_total | counter | Total number of results in which a connection was successfully established | key, group, name, type | All |
|
||||
| gatus_results_duration_seconds | gauge | Duration of the request in seconds | key, group, name, type | All |
|
||||
| gatus_results_certificate_expiration_seconds | gauge | Number of seconds until the certificate expires | key, group, name, type | HTTP, STARTTLS |
|
||||
|
||||
See [examples/docker-compose-grafana-prometheus](.examples/docker-compose-grafana-prometheus) for further documentation as well as an example.
|
||||
|
||||
|
||||
## Deployment
|
||||
Many examples can be found in the [.examples](.examples) folder, but this section will focus on the most popular ways of deploying Gatus.
|
||||
|
||||
@@ -1008,7 +1152,7 @@ will send a `POST` request to `http://localhost:8080/playground` with the follow
|
||||
> tells Gatus to only evaluate one endpoint at a time.
|
||||
|
||||
To ensure that Gatus provides reliable and accurate results (i.e. response time), Gatus only evaluates one endpoint at a time
|
||||
In other words, even if you have multiple endpoints with the exact same interval, they will not execute at the same time.
|
||||
In other words, even if you have multiple endpoints with the same interval, they will not execute at the same time.
|
||||
|
||||
You can test this yourself by running Gatus with several endpoints configured with a very short, unrealistic interval,
|
||||
such as 1ms. You'll notice that the response time does not fluctuate - that is because while endpoints are evaluated on
|
||||
@@ -1026,19 +1170,19 @@ to respect the configured interval, for instance:
|
||||
- Endpoint B has an interval of 5s, and takes 1ms to complete
|
||||
- Endpoint B will be unable to run every 5s, because endpoint A's health evaluation takes longer than its interval
|
||||
|
||||
To sum it up, while Gatus can really handle any interval you throw at it, you're better off having slow requests with
|
||||
To sum it up, while Gatus can handle any interval you throw at it, you're better off having slow requests with
|
||||
higher interval.
|
||||
|
||||
As a rule of the thumb, I personally set interval for more complex health checks to `5m` (5 minutes) and
|
||||
As a rule of thumb, I personally set the interval for more complex health checks to `5m` (5 minutes) and
|
||||
simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
|
||||
|
||||
|
||||
### Default timeouts
|
||||
| Endpoint type | Timeout |
|
||||
|:------------- |:------- |
|
||||
| HTTP | 10s
|
||||
| TCP | 10s
|
||||
| ICMP | 10s
|
||||
| Endpoint type | Timeout |
|
||||
|:---------------|:--------|
|
||||
| HTTP | 10s |
|
||||
| TCP | 10s |
|
||||
| ICMP | 10s |
|
||||
|
||||
To modify the timeout, see [Client configuration](#client-configuration).
|
||||
|
||||
@@ -1058,6 +1202,8 @@ endpoints:
|
||||
Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `endpoints[].headers`,
|
||||
`endpoints[].method` and `endpoints[].graphql` are not supported for TCP endpoints.
|
||||
|
||||
This works for applications such as databases (Postgres, MySQL, etc.) and caches (Redis, Memcached, etc.).
|
||||
|
||||
**NOTE**: `[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
|
||||
established.
|
||||
@@ -1141,7 +1287,7 @@ There are three main reasons why you might want to disable the monitoring lock:
|
||||
- You're using Gatus for load testing (each endpoint are periodically evaluated on a different goroutine, so
|
||||
technically, if you create 100 endpoints with a 1 seconds interval, Gatus will send 100 requests per second)
|
||||
- You have a _lot_ of endpoints to monitor
|
||||
- You want to test multiple endpoints at very short interval (< 5s)
|
||||
- You want to test multiple endpoints at very short intervals (< 5s)
|
||||
|
||||
|
||||
### Reloading configuration on the fly
|
||||
@@ -1229,13 +1375,13 @@ web:
|
||||
```
|
||||
|
||||
### Badges
|
||||
### Uptime
|
||||
#### Uptime
|
||||

|
||||

|
||||

|
||||
|
||||
Gatus can automatically generate a SVG badge for one of your monitored endpoints.
|
||||
This allows you to put badges in your individual applications' README or even create your own status page, if you
|
||||
Gatus can automatically generate an SVG badge for one of your monitored endpoints.
|
||||
This allows you to put badges in your individual applications' README or even create your own status page if you
|
||||
desire.
|
||||
|
||||
The path to generate a badge is the following:
|
||||
@@ -1259,10 +1405,27 @@ Example:
|
||||
```
|
||||

|
||||
```
|
||||
If you'd like to see a visual example of each badges available, you can simply navigate to the endpoint's detail page.
|
||||
If you'd like to see a visual example of each badge available, you can simply navigate to the endpoint's detail page.
|
||||
|
||||
|
||||
### Response time
|
||||
#### Health
|
||||

|
||||
|
||||
The path to generate a badge is the following:
|
||||
```
|
||||
/api/v1/endpoints/{key}/health/badge.svg
|
||||
```
|
||||
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.svg
|
||||
```
|
||||
|
||||
|
||||
#### Response time
|
||||

|
||||

|
||||

|
||||
@@ -1277,7 +1440,7 @@ Where:
|
||||
|
||||
|
||||
### API
|
||||
Gatus provides a simple read-only API which can be queried in order to programmatically determine endpoint status and history.
|
||||
Gatus provides a simple read-only API that can be queried in order to programmatically determine endpoint status and history.
|
||||
|
||||
All endpoints are available via a GET request to the following endpoint:
|
||||
```
|
||||
|
||||
@@ -14,6 +14,9 @@ const (
|
||||
// TypeEmail is the Type for the email alerting provider
|
||||
TypeEmail Type = "email"
|
||||
|
||||
// TypeGoogleChat is the Type for the googlechat alerting provider
|
||||
TypeGoogleChat Type = "googlechat"
|
||||
|
||||
// TypeMattermost is the Type for the mattermost alerting provider
|
||||
TypeMattermost Type = "mattermost"
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/opsgenie"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/slack"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/teams"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/twilio"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/googlechat"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/opsgenie"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/pagerduty"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/slack"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/teams"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/twilio"
|
||||
)
|
||||
|
||||
// Config is the configuration for alerting providers
|
||||
@@ -21,6 +22,9 @@ type Config struct {
|
||||
// Custom is the configuration for the custom alerting provider
|
||||
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
|
||||
|
||||
// googlechat is the configuration for the Google chat alerting provider
|
||||
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
|
||||
|
||||
// Discord is the configuration for the discord alerting provider
|
||||
Discord *discord.AlertProvider `yaml:"discord,omitempty"`
|
||||
|
||||
@@ -73,6 +77,12 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid
|
||||
return nil
|
||||
}
|
||||
return config.Email
|
||||
case alert.TypeGoogleChat:
|
||||
if config.GoogleChat == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.GoogleChat
|
||||
case alert.TypeMattermost:
|
||||
if config.Mattermost == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
||||
@@ -22,10 +22,10 @@ type AlertProvider struct {
|
||||
Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"`
|
||||
|
||||
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||
ClientConfig *client.Config `yaml:"client"`
|
||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -50,48 +50,28 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
|
||||
return status
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription string, resolved bool) *http.Request {
|
||||
body := provider.Body
|
||||
providerURL := provider.URL
|
||||
method := provider.Method
|
||||
|
||||
if strings.Contains(body, "[ALERT_DESCRIPTION]") {
|
||||
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alertDescription)
|
||||
}
|
||||
if strings.Contains(body, "[SERVICE_NAME]") { // XXX: Remove this in v4.0.0
|
||||
body = strings.ReplaceAll(body, "[SERVICE_NAME]", endpointName)
|
||||
}
|
||||
if strings.Contains(body, "[ENDPOINT_NAME]") {
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpointName)
|
||||
}
|
||||
if strings.Contains(body, "[ALERT_TRIGGERED_OR_RESOLVED]") {
|
||||
if resolved {
|
||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||
} else {
|
||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
|
||||
}
|
||||
}
|
||||
if strings.Contains(providerURL, "[ALERT_DESCRIPTION]") {
|
||||
providerURL = strings.ReplaceAll(providerURL, "[ALERT_DESCRIPTION]", alertDescription)
|
||||
}
|
||||
if strings.Contains(providerURL, "[SERVICE_NAME]") { // XXX: Remove this in v4.0.0
|
||||
providerURL = strings.ReplaceAll(providerURL, "[SERVICE_NAME]", endpointName)
|
||||
}
|
||||
if strings.Contains(providerURL, "[ENDPOINT_NAME]") {
|
||||
providerURL = strings.ReplaceAll(providerURL, "[ENDPOINT_NAME]", endpointName)
|
||||
}
|
||||
if strings.Contains(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]") {
|
||||
if resolved {
|
||||
providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||
} else {
|
||||
providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
|
||||
}
|
||||
func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
||||
body, url, method := provider.Body, provider.URL, provider.Method
|
||||
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
|
||||
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
|
||||
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
|
||||
if resolved {
|
||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||
} else {
|
||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
|
||||
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
|
||||
}
|
||||
if len(method) == 0 {
|
||||
method = http.MethodGet
|
||||
}
|
||||
bodyBuffer := bytes.NewBuffer([]byte(body))
|
||||
request, _ := http.NewRequest(method, providerURL, bodyBuffer)
|
||||
request, _ := http.NewRequest(method, url, bodyBuffer)
|
||||
for k, v := range provider.Headers {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
@@ -99,7 +79,7 @@ func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription s
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
request := provider.buildHTTPRequest(endpoint.Name, alert.GetDescription(), resolved)
|
||||
request := provider.buildHTTPRequest(endpoint, alert, resolved)
|
||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package custom
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
@@ -99,77 +100,103 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildHTTPRequestWhenResolved(t *testing.T) {
|
||||
const (
|
||||
ExpectedURL = "https://example.com/endpoint-name?event=RESOLVED&description=alert-description"
|
||||
ExpectedBody = "endpoint-name,alert-description,RESOLVED"
|
||||
)
|
||||
func TestAlertProvider_buildHTTPRequest(t *testing.T) {
|
||||
customAlertProvider := &AlertProvider{
|
||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
Headers: nil,
|
||||
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]",
|
||||
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
}
|
||||
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true)
|
||||
if request.URL.String() != ExpectedURL {
|
||||
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String())
|
||||
alertDescription := "alert-description"
|
||||
scenarios := []struct {
|
||||
AlertProvider *AlertProvider
|
||||
Resolved bool
|
||||
ExpectedURL string
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
AlertProvider: customAlertProvider,
|
||||
Resolved: true,
|
||||
ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=RESOLVED&description=alert-description&url=https://example.com",
|
||||
ExpectedBody: "endpoint-name,endpoint-group,alert-description,https://example.com,RESOLVED",
|
||||
},
|
||||
{
|
||||
AlertProvider: customAlertProvider,
|
||||
Resolved: false,
|
||||
ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=TRIGGERED&description=alert-description&url=https://example.com",
|
||||
ExpectedBody: "endpoint-name,endpoint-group,alert-description,https://example.com,TRIGGERED",
|
||||
},
|
||||
}
|
||||
body, _ := io.ReadAll(request.Body)
|
||||
if string(body) != ExpectedBody {
|
||||
t.Error("expected body to be", ExpectedBody, "was", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildHTTPRequestWhenTriggered(t *testing.T) {
|
||||
const (
|
||||
ExpectedURL = "https://example.com/endpoint-name?event=TRIGGERED&description=alert-description"
|
||||
ExpectedBody = "endpoint-name,alert-description,TRIGGERED"
|
||||
)
|
||||
customAlertProvider := &AlertProvider{
|
||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
Headers: map[string]string{"Authorization": "Basic hunter2"},
|
||||
}
|
||||
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", false)
|
||||
if request.URL.String() != ExpectedURL {
|
||||
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String())
|
||||
}
|
||||
body, _ := io.ReadAll(request.Body)
|
||||
if string(body) != ExpectedBody {
|
||||
t.Error("expected body to be", ExpectedBody, "was", string(body))
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||
request := customAlertProvider.buildHTTPRequest(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
||||
&alert.Alert{Description: &alertDescription},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if request.URL.String() != scenario.ExpectedURL {
|
||||
t.Error("expected URL to be", scenario.ExpectedURL, "got", request.URL.String())
|
||||
}
|
||||
body, _ := io.ReadAll(request.Body)
|
||||
if string(body) != scenario.ExpectedBody {
|
||||
t.Error("expected body to be", scenario.ExpectedBody, "got", string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
||||
const (
|
||||
ExpectedURL = "https://example.com/endpoint-name?event=test&description=alert-description"
|
||||
ExpectedBody = "endpoint-name,alert-description,test"
|
||||
)
|
||||
customAlertProvider := &AlertProvider{
|
||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
Headers: nil,
|
||||
Placeholders: map[string]map[string]string{
|
||||
"ALERT_TRIGGERED_OR_RESOLVED": {
|
||||
"RESOLVED": "test",
|
||||
"RESOLVED": "fixed",
|
||||
"TRIGGERED": "boom",
|
||||
},
|
||||
},
|
||||
}
|
||||
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true)
|
||||
if request.URL.String() != ExpectedURL {
|
||||
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String())
|
||||
alertDescription := "alert-description"
|
||||
scenarios := []struct {
|
||||
AlertProvider *AlertProvider
|
||||
Resolved bool
|
||||
ExpectedURL string
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
AlertProvider: customAlertProvider,
|
||||
Resolved: true,
|
||||
ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=fixed&description=alert-description",
|
||||
ExpectedBody: "endpoint-name,endpoint-group,alert-description,fixed",
|
||||
},
|
||||
{
|
||||
AlertProvider: customAlertProvider,
|
||||
Resolved: false,
|
||||
ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=boom&description=alert-description",
|
||||
ExpectedBody: "endpoint-name,endpoint-group,alert-description,boom",
|
||||
},
|
||||
}
|
||||
body, _ := io.ReadAll(request.Body)
|
||||
if string(body) != ExpectedBody {
|
||||
t.Error("expected body to be", ExpectedBody, "was", string(body))
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||
request := customAlertProvider.buildHTTPRequest(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&alert.Alert{Description: &alertDescription},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if request.URL.String() != scenario.ExpectedURL {
|
||||
t.Error("expected URL to be", scenario.ExpectedURL, "got", request.URL.String())
|
||||
}
|
||||
body, _ := io.ReadAll(request.Body)
|
||||
if string(body) != scenario.ExpectedBody {
|
||||
t.Error("expected body to be", scenario.ExpectedBody, "got", string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) {
|
||||
customAlertProvider := &AlertProvider{
|
||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
Headers: nil,
|
||||
Placeholders: nil,
|
||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
}
|
||||
if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" {
|
||||
t.Error("expected RESOLVED, got", customAlertProvider.GetAlertStatePlaceholderValue(true))
|
||||
@@ -187,26 +214,3 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAlertProvider_isBackwardCompatibleWithServiceRename checks if the custom alerting provider still supports
|
||||
// service placeholders after the migration from "service" to "endpoint"
|
||||
//
|
||||
// XXX: Remove this in v4.0.0
|
||||
func TestAlertProvider_isBackwardCompatibleWithServiceRename(t *testing.T) {
|
||||
const (
|
||||
ExpectedURL = "https://example.com/endpoint-name?event=TRIGGERED&description=alert-description"
|
||||
ExpectedBody = "endpoint-name,alert-description,TRIGGERED"
|
||||
)
|
||||
customAlertProvider := &AlertProvider{
|
||||
URL: "https://example.com/[SERVICE_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||
Body: "[SERVICE_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||
}
|
||||
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", false)
|
||||
if request.URL.String() != ExpectedURL {
|
||||
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String())
|
||||
}
|
||||
body, _ := io.ReadAll(request.Body)
|
||||
if string(body) != ExpectedBody {
|
||||
t.Error("expected body to be", ExpectedBody, "was", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||
@@ -16,18 +16,36 @@ type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
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"`
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
}
|
||||
|
||||
// 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.WebhookURL) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,6 +104,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}`, message, description, colorCode, results)
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.WebhookURL
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.WebhookURL
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
@@ -22,6 +22,43 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
providerWithInvalidOverrideTo := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideTo.IsValid() {
|
||||
t.Error("provider integration key shouldn't have been valid")
|
||||
}
|
||||
providerWithValidOverride := AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
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"
|
||||
@@ -156,3 +193,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example01.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getWebhookURLForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getWebhookURLForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using SMTP
|
||||
type AlertProvider struct {
|
||||
From string `yaml:"from"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
@@ -20,22 +21,47 @@ type AlertProvider struct {
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||
Overrides []Override `yaml:"overrides,omitempty"`
|
||||
}
|
||||
|
||||
// Override is a case under which the default integration is overridden
|
||||
type Override struct {
|
||||
Group string `yaml:"group"`
|
||||
To string `yaml:"to"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
func (provider *AlertProvider) IsValid() bool {
|
||||
registeredGroups := make(map[string]bool)
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.To) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
|
||||
return len(provider.From) > 0 && len(provider.Password) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
var username string
|
||||
if len(provider.Username) > 0 {
|
||||
username = provider.Username
|
||||
} else {
|
||||
username = provider.From
|
||||
}
|
||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", provider.From)
|
||||
m.SetHeader("To", strings.Split(provider.To, ",")...)
|
||||
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/plain", body)
|
||||
d := gomail.NewDialer(provider.Host, provider.Port, provider.From, provider.Password)
|
||||
d := gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
|
||||
return d.DialAndSend(m)
|
||||
}
|
||||
|
||||
@@ -65,6 +91,18 @@ func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoin
|
||||
return subject, message + description + "\n\nCondition results:\n" + results
|
||||
}
|
||||
|
||||
// getToForGroup returns the appropriate email integration to for a given group
|
||||
func (provider *AlertProvider) getToForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.To
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.To
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
|
||||
@@ -3,11 +3,11 @@ package email
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
invalidProvider := AlertProvider{}
|
||||
if invalidProvider.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
@@ -18,6 +18,47 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
To: "to@example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
providerWithInvalidOverrideTo := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
To: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideTo.IsValid() {
|
||||
t.Error("provider integration key shouldn't have been valid")
|
||||
}
|
||||
providerWithValidOverride := AlertProvider{
|
||||
From: "from@example.com",
|
||||
Password: "password",
|
||||
Host: "smtp.gmail.com",
|
||||
Port: 587,
|
||||
To: "to@example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
To: "to@example.com",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if !providerWithValidOverride.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
@@ -77,3 +118,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_getToForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "to@example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "to@example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
To: "to01@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "to@example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
To: "to01@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "to01@example.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getToForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
156
alerting/provider/googlechat/googlechat.go
Normal file
156
alerting/provider/googlechat/googlechat.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package googlechat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
||||
type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
|
||||
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
// 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"`
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
func (provider *AlertProvider) IsValid() bool {
|
||||
if provider.ClientConfig == nil {
|
||||
provider.ClientConfig = client.GetDefaultConfig()
|
||||
}
|
||||
|
||||
registeredGroups := make(map[string]bool)
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
|
||||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
if resolved {
|
||||
color = "#36A64F"
|
||||
message = fmt.Sprintf("<font color='%s'>An alert has been resolved after passing successfully %d time(s) in a row</font>", color, alert.SuccessThreshold)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
var results string
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = "✅"
|
||||
} else {
|
||||
prefix = "❌"
|
||||
}
|
||||
results += fmt.Sprintf("%s %s<br>", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":: " + alertDescription
|
||||
}
|
||||
return fmt.Sprintf(`{
|
||||
"cards": [
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"widgets": [
|
||||
{
|
||||
"keyValue": {
|
||||
"topLabel": "%s [%s]",
|
||||
"content": "%s",
|
||||
"contentMultiline": "true",
|
||||
"bottomLabel": "%s",
|
||||
"icon": "BOOKMARK"
|
||||
}
|
||||
},
|
||||
{
|
||||
"keyValue": {
|
||||
"topLabel": "Condition results",
|
||||
"content": "%s",
|
||||
"contentMultiline": "true",
|
||||
"icon": "DESCRIPTION"
|
||||
}
|
||||
},
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"textButton": {
|
||||
"text": "URL",
|
||||
"onClick": {
|
||||
"openLink": {
|
||||
"url": "%s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, endpoint.Name, endpoint.Group, message, description, results, endpoint.URL)
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.WebhookURL
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.WebhookURL
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
260
alerting/provider/googlechat/googlechat_test.go
Normal file
260
alerting/provider/googlechat/googlechat_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package googlechat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||
if invalidProvider.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
}
|
||||
validProvider := AlertProvider{WebhookURL: "http://example.com"}
|
||||
if !validProvider.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
providerWithInvalidOverrideTo := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideTo.IsValid() {
|
||||
t.Error("provider integration key shouldn't have been valid")
|
||||
}
|
||||
providerWithValidOverride := AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
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(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.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
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"cards\": [\n {\n \"sections\": [\n {\n \"widgets\": [\n {\n \"keyValue\": {\n \"topLabel\": \"endpoint-name []\",\n \"content\": \"\u003cfont color='#DD0000'\u003eAn alert has been triggered due to having failed 3 time(s) in a row\u003c/font\u003e\",\n \"contentMultiline\": \"true\",\n \"bottomLabel\": \":: description-1\",\n \"icon\": \"BOOKMARK\"\n }\n },\n {\n \"keyValue\": {\n \"topLabel\": \"Condition results\",\n \"content\": \"❌ [CONNECTED] == true\u003cbr\u003e❌ [STATUS] == 200\u003cbr\u003e\",\n \"contentMultiline\": \"true\",\n \"icon\": \"DESCRIPTION\"\n }\n },\n {\n \"buttons\": [\n {\n \"textButton\": {\n \"text\": \"URL\",\n \"onClick\": {\n \"openLink\": {\n \"url\": \"\"\n }\n }\n }\n }\n ]\n }\n ]\n }\n ]\n }\n]\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"cards\": [\n {\n \"sections\": [\n {\n \"widgets\": [\n {\n \"keyValue\": {\n \"topLabel\": \"endpoint-name []\",\n \"content\": \"\u003cfont color='#36A64F'\u003eAn alert has been resolved after passing successfully 5 time(s) in a row\u003c/font\u003e\",\n \"contentMultiline\": \"true\",\n \"bottomLabel\": \":: description-2\",\n \"icon\": \"BOOKMARK\"\n }\n },\n {\n \"keyValue\": {\n \"topLabel\": \"Condition results\",\n \"content\": \"✅ [CONNECTED] == true\u003cbr\u003e✅ [STATUS] == 200\u003cbr\u003e\",\n \"contentMultiline\": \"true\",\n \"icon\": \"DESCRIPTION\"\n }\n },\n {\n \"buttons\": [\n {\n \"textButton\": {\n \"text\": \"URL\",\n \"onClick\": {\n \"openLink\": {\n \"url\": \"\"\n }\n }\n }\n }\n ]\n }\n ]\n }\n ]\n }\n]\n}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
b, _ := json.Marshal(body)
|
||||
e, _ := json.Marshal(scenario.ExpectedBody)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", e, b)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(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_getWebhookURLForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example01.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getWebhookURLForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
||||
@@ -16,10 +16,19 @@ type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
|
||||
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||
ClientConfig *client.Config `yaml:"client"`
|
||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
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"`
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -27,13 +36,22 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
if provider.ClientConfig == nil {
|
||||
provider.ClientConfig = client.GetDefaultConfig()
|
||||
}
|
||||
if provider.Overrides != nil {
|
||||
registeredGroups := make(map[string]bool)
|
||||
for _, override := range provider.Overrides {
|
||||
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,6 +119,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}`, message, message, description, color, endpoint.URL, results)
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.WebhookURL
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.WebhookURL
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
@@ -22,6 +22,47 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
|
||||
providerWithInvalidOverrideWebHookUrl := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
|
||||
WebhookURL: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideWebHookUrl.IsValid() {
|
||||
t.Error("provider WebHookURL shoudn't have been valid")
|
||||
}
|
||||
|
||||
providerWithValidOverride := AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
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"
|
||||
@@ -156,3 +197,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example01.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getWebhookURLForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestMessagebirdAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/slack"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/teams"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/twilio"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/googlechat"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/opsgenie"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/pagerduty"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/slack"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/teams"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/twilio"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the interface that each providers should implement
|
||||
@@ -54,8 +56,10 @@ var (
|
||||
_ AlertProvider = (*custom.AlertProvider)(nil)
|
||||
_ AlertProvider = (*discord.AlertProvider)(nil)
|
||||
_ AlertProvider = (*email.AlertProvider)(nil)
|
||||
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
||||
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
||||
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
||||
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
|
||||
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
|
||||
_ AlertProvider = (*slack.AlertProvider)(nil)
|
||||
_ AlertProvider = (*teams.AlertProvider)(nil)
|
||||
|
||||
@@ -3,7 +3,7 @@ package provider
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
)
|
||||
|
||||
func TestParseWithDefaultAlert(t *testing.T) {
|
||||
|
||||
@@ -6,28 +6,44 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||
type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"` // Slack webhook URL
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
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"`
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
}
|
||||
|
||||
// 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.WebhookURL) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,6 +102,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}`, message, description, color, results)
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.WebhookURL
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.WebhookURL
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||
if invalidProvider.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
@@ -20,8 +20,44 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
if !validProvider.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
providerWithInvalidOverrideTo := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideTo.IsValid() {
|
||||
t.Error("provider integration key shouldn't have been valid")
|
||||
}
|
||||
providerWithValidOverride := AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
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"
|
||||
@@ -79,7 +115,7 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "name"},
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
@@ -175,3 +211,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example01.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getWebhookURLForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getWebhookURLForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Teams
|
||||
@@ -16,18 +16,36 @@ type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
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"`
|
||||
WebhookURL string `yaml:"webhook-url"`
|
||||
}
|
||||
|
||||
// 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.WebhookURL) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,6 +104,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}`, color, message, description, endpoint.URL, results)
|
||||
}
|
||||
|
||||
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.WebhookURL
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.WebhookURL
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||
if invalidProvider.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
@@ -22,6 +22,43 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
providerWithInvalidOverrideTo := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideTo.IsValid() {
|
||||
t.Error("provider integration key shouldn't have been valid")
|
||||
}
|
||||
providerWithValidOverride := AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
WebhookURL: "http://example.com",
|
||||
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"
|
||||
@@ -156,3 +193,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "http://example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
WebhookURL: "http://example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
WebhookURL: "http://example01.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "http://example01.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getWebhookURLForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,21 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
const defaultAPIURL = "https://api.telegram.org"
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Telegram
|
||||
type AlertProvider struct {
|
||||
Token string `yaml:"token"`
|
||||
ID string `yaml:"id"`
|
||||
Token string `yaml:"token"`
|
||||
ID string `yaml:"id"`
|
||||
APIURL string `yaml:"api-url"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
@@ -28,7 +31,11 @@ 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)))
|
||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), buffer)
|
||||
apiURL := provider.APIURL
|
||||
if apiURL == "" {
|
||||
apiURL = defaultAPIURL
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/bot%s/sendMessage", apiURL, provider.Token), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/test"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
||||
@@ -21,7 +21,7 @@ type AlertProvider struct {
|
||||
To string `yaml:"to"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
|
||||
@@ -3,8 +3,8 @@ package twilio
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v4/test"
|
||||
)
|
||||
|
||||
func TestGetHTTPClient(t *testing.T) {
|
||||
@@ -10,8 +15,18 @@ func TestGetHTTPClient(t *testing.T) {
|
||||
Insecure: false,
|
||||
IgnoreRedirect: false,
|
||||
Timeout: 0,
|
||||
DNSResolver: "tcp://1.1.1.1:53",
|
||||
OAuth2Config: &OAuth2Config{
|
||||
ClientID: "00000000-0000-0000-0000-000000000000",
|
||||
ClientSecret: "secretsauce",
|
||||
TokenURL: "https://token-server.local/token",
|
||||
Scopes: []string{"https://application.local/.default"},
|
||||
},
|
||||
}
|
||||
err := cfg.ValidateAndSetDefaults()
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, but got: `%s`", err)
|
||||
}
|
||||
cfg.ValidateAndSetDefaults()
|
||||
if GetHTTPClient(cfg) == nil {
|
||||
t.Error("expected client to not be nil")
|
||||
}
|
||||
@@ -146,3 +161,61 @@ func TestCanCreateTCPConnection(t *testing.T) {
|
||||
t.Error("should've failed, because there's no port in the address")
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks if a HTTP client configured with `configureOAuth2()` automatically
|
||||
// performs a Client Credentials OAuth2 flow and adds the obtained token as a `Authorization`
|
||||
// header to all outgoing HTTP calls.
|
||||
func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
|
||||
defer InjectHTTPClient(nil)
|
||||
oAuth2Config := &OAuth2Config{
|
||||
ClientID: "00000000-0000-0000-0000-000000000000",
|
||||
ClientSecret: "secretsauce",
|
||||
TokenURL: "https://token-server.local/token",
|
||||
Scopes: []string{"https://application.local/.default"},
|
||||
}
|
||||
mockHttpClient := &http.Client{
|
||||
Transport: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||
// if the mock HTTP client tries to get a token from the `token-server`
|
||||
// we provide the expected token response
|
||||
if r.Host == "token-server.local" {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader(
|
||||
[]byte(
|
||||
`{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"secret-token"}`,
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
// to verify the headers were sent as expected, we echo them back in the
|
||||
// `X-Org-Authorization` header and check if the token value matches our
|
||||
// mocked `token-server` response
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: map[string][]string{
|
||||
"X-Org-Authorization": {r.Header.Get("Authorization")},
|
||||
},
|
||||
Body: http.NoBody,
|
||||
}
|
||||
}),
|
||||
}
|
||||
mockHttpClientWithOAuth := configureOAuth2(mockHttpClient, *oAuth2Config)
|
||||
InjectHTTPClient(mockHttpClientWithOAuth)
|
||||
request, err := http.NewRequest(http.MethodPost, "http://127.0.0.1:8282", http.NoBody)
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
response, err := mockHttpClientWithOAuth.Do(request)
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if response.Header == nil {
|
||||
t.Error("expected response headers, but got nil")
|
||||
}
|
||||
// the mock response echos the Authorization header used in the request back
|
||||
// to us as `X-Org-Authorization` header, we check here if the value matches
|
||||
// our expected token `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"))
|
||||
}
|
||||
}
|
||||
|
||||
134
client/config.go
134
client/config.go
@@ -1,9 +1,18 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -11,7 +20,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultConfig is the default client configuration
|
||||
ErrInvalidDNSResolver = errors.New("invalid DNS resolver specified. Required format is {proto}://{ip}:{port}")
|
||||
ErrInvalidDNSResolverPort = errors.New("invalid DNS resolver port")
|
||||
ErrInvalidClientOAuth2Config = errors.New("invalid OAuth2 configuration, all fields are required")
|
||||
|
||||
defaultConfig = Config{
|
||||
Insecure: false,
|
||||
IgnoreRedirect: false,
|
||||
@@ -28,22 +40,99 @@ func GetDefaultConfig() *Config {
|
||||
// Config is the configuration for clients
|
||||
type Config struct {
|
||||
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
||||
Insecure bool `yaml:"insecure"`
|
||||
Insecure bool `yaml:"insecure,omitempty"`
|
||||
|
||||
// IgnoreRedirect determines whether to ignore redirects (true) or follow them (false, default)
|
||||
IgnoreRedirect bool `yaml:"ignore-redirect"`
|
||||
IgnoreRedirect bool `yaml:"ignore-redirect,omitempty"`
|
||||
|
||||
// Timeout for the client
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
|
||||
// DNSResolver override for the HTTP client
|
||||
// Expected format is {protocol}://{host}:{port}, e.g. tcp://1.1.1.1:53
|
||||
DNSResolver string `yaml:"dns-resolver,omitempty"`
|
||||
|
||||
// OAuth2Config is the OAuth2 configuration used for the client.
|
||||
//
|
||||
// If non-nil, the http.Client returned by getHTTPClient will automatically retrieve a token if necessary.
|
||||
// See configureOAuth2 for more details.
|
||||
OAuth2Config *OAuth2Config `yaml:"oauth2,omitempty"`
|
||||
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
|
||||
type DNSResolverConfig struct {
|
||||
Protocol string
|
||||
Host string
|
||||
Port string
|
||||
}
|
||||
|
||||
// OAuth2Config is the configuration for the OAuth2 client credentials flow
|
||||
type OAuth2Config struct {
|
||||
TokenURL string `yaml:"token-url"` // e.g. https://dev-12345678.okta.com/token
|
||||
ClientID string `yaml:"client-id"`
|
||||
ClientSecret string `yaml:"client-secret"`
|
||||
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
|
||||
func (c *Config) ValidateAndSetDefaults() {
|
||||
func (c *Config) ValidateAndSetDefaults() error {
|
||||
if c.Timeout < time.Millisecond {
|
||||
c.Timeout = 10 * time.Second
|
||||
}
|
||||
if c.HasCustomDNSResolver() {
|
||||
// Validate the DNS resolver now to make sure it will not return an error later.
|
||||
if _, err := c.parseDNSResolver(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.HasOAuth2Config() && !c.OAuth2Config.isValid() {
|
||||
return ErrInvalidClientOAuth2Config
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasCustomDNSResolver returns whether a custom DNSResolver is configured
|
||||
func (c *Config) HasCustomDNSResolver() bool {
|
||||
return len(c.DNSResolver) > 0
|
||||
}
|
||||
|
||||
// parseDNSResolver parses the DNS resolver into the DNSResolverConfig struct
|
||||
func (c *Config) parseDNSResolver() (*DNSResolverConfig, error) {
|
||||
re := regexp.MustCompile(`^(?P<proto>(.*))://(?P<host>[A-Za-z0-9\-\.]+):(?P<port>[0-9]+)?(.*)$`)
|
||||
matches := re.FindStringSubmatch(c.DNSResolver)
|
||||
if len(matches) == 0 {
|
||||
return nil, ErrInvalidDNSResolver
|
||||
}
|
||||
r := make(map[string]string)
|
||||
for i, k := range re.SubexpNames() {
|
||||
if i != 0 && k != "" {
|
||||
r[k] = matches[i]
|
||||
}
|
||||
}
|
||||
port, err := strconv.Atoi(r["port"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return nil, ErrInvalidDNSResolverPort
|
||||
}
|
||||
return &DNSResolverConfig{
|
||||
Protocol: r["proto"],
|
||||
Host: r["host"],
|
||||
Port: r["port"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HasOAuth2Config returns true if the client has OAuth2 configuration parameters
|
||||
func (c *Config) HasOAuth2Config() bool {
|
||||
return c.OAuth2Config != nil
|
||||
}
|
||||
|
||||
// isValid() returns true if the OAuth2 configuration is valid
|
||||
func (c *OAuth2Config) isValid() bool {
|
||||
return len(c.TokenURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0
|
||||
}
|
||||
|
||||
// GetHTTPClient return an HTTP client matching the Config's parameters.
|
||||
@@ -68,6 +157,43 @@ func (c *Config) getHTTPClient() *http.Client {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if c.HasCustomDNSResolver() {
|
||||
dnsResolver, err := c.parseDNSResolver()
|
||||
if err != nil {
|
||||
// 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 ;)
|
||||
log.Println("[client][getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring invalid DNS resolver due to error:", err.Error())
|
||||
} else {
|
||||
dialer := &net.Dialer{
|
||||
Resolver: &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{}
|
||||
return d.DialContext(ctx, dnsResolver.Protocol, dnsResolver.Host+":"+dnsResolver.Port)
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClient.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.HasOAuth2Config() {
|
||||
c.httpClient = configureOAuth2(c.httpClient, *c.OAuth2Config)
|
||||
}
|
||||
}
|
||||
return c.httpClient
|
||||
}
|
||||
|
||||
// configureOAuth2 returns an HTTP client that will obtain and refresh tokens as necessary.
|
||||
// The returned Client and its Transport should not be modified.
|
||||
func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
|
||||
oauth2cfg := clientcredentials.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
Scopes: c.Scopes,
|
||||
TokenURL: c.TokenURL,
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||
return oauth2cfg.Client(ctx)
|
||||
}
|
||||
|
||||
@@ -35,3 +35,47 @@ func TestConfig_getHTTPClient(t *testing.T) {
|
||||
t.Error("expected Config.IgnoreRedirect set to true to cause the HTTP client's CheckRedirect to return http.ErrUseLastResponse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ValidateAndSetDefaults_withCustomDNSResolver(t *testing.T) {
|
||||
type args struct {
|
||||
dnsResolver string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "with-valid-resolver",
|
||||
args: args{
|
||||
dnsResolver: "tcp://1.1.1.1:53",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with-invalid-resolver-port",
|
||||
args: args{
|
||||
dnsResolver: "tcp://127.0.0.1:99999",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with-invalid-resolver-format",
|
||||
args: args{
|
||||
dnsResolver: "foobar",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
DNSResolver: tt.args.dnsResolver,
|
||||
}
|
||||
err := cfg.ValidateAndSetDefaults()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateAndSetDefaults() error=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ endpoints:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
- name: example-dns-query
|
||||
url: "8.8.8.8" # Address of the DNS server to use
|
||||
url: "1.1.1.1" # Address of the DNS server to use
|
||||
interval: 5m
|
||||
dns:
|
||||
query-name: "example.com"
|
||||
|
||||
@@ -6,15 +6,15 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting"
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider"
|
||||
"github.com/TwiN/gatus/v3/config/maintenance"
|
||||
"github.com/TwiN/gatus/v3/config/ui"
|
||||
"github.com/TwiN/gatus/v3/config/web"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/security"
|
||||
"github.com/TwiN/gatus/v3/storage"
|
||||
"github.com/TwiN/gatus/v4/alerting"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider"
|
||||
"github.com/TwiN/gatus/v4/config/maintenance"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/config/web"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/security"
|
||||
"github.com/TwiN/gatus/v4/storage"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,22 +5,22 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting"
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/slack"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/teams"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/twilio"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/config/ui"
|
||||
"github.com/TwiN/gatus/v3/config/web"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage"
|
||||
"github.com/TwiN/gatus/v4/alerting"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/pagerduty"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/slack"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/teams"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/telegram"
|
||||
"github.com/TwiN/gatus/v4/alerting/provider/twilio"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/config/web"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage"
|
||||
)
|
||||
|
||||
func TestLoadFileThatDoesNotExist(t *testing.T) {
|
||||
@@ -53,7 +53,14 @@ maintenance:
|
||||
duration: 4h
|
||||
every: [Monday, Thursday]
|
||||
ui:
|
||||
title: Test
|
||||
title: T
|
||||
header: H
|
||||
link: https://example.org
|
||||
buttons:
|
||||
- name: "Home"
|
||||
link: "https://example.org"
|
||||
- name: "Status page"
|
||||
link: "https://status.example.org"
|
||||
endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/health
|
||||
@@ -88,8 +95,8 @@ endpoints:
|
||||
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite {
|
||||
t.Error("expected storage to be set to sqlite, got", config.Storage)
|
||||
}
|
||||
if config.UI == nil || config.UI.Title != "Test" {
|
||||
t.Error("Expected Config.UI.Title to be Test")
|
||||
if config.UI == nil || config.UI.Title != "T" || config.UI.Header != "H" || config.UI.Link != "https://example.org" || len(config.UI.Buttons) != 2 || config.UI.Buttons[0].Name != "Home" || config.UI.Buttons[0].Link != "https://example.org" || config.UI.Buttons[1].Name != "Status page" || config.UI.Buttons[1].Link != "https://status.example.org" {
|
||||
t.Error("expected ui to be set to T, H, https://example.org, 2 buttons, Home and Status page, got", config.UI)
|
||||
}
|
||||
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")
|
||||
@@ -1172,12 +1179,12 @@ endpoints:
|
||||
|
||||
func TestParseAndValidateConfigBytesWithValidSecurityConfig(t *testing.T) {
|
||||
const expectedUsername = "admin"
|
||||
const expectedPasswordHash = "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22"
|
||||
const expectedPasswordHash = "JDJhJDEwJHRiMnRFakxWazZLdXBzRERQazB1TE8vckRLY05Yb1hSdnoxWU0yQ1FaYXZRSW1McmladDYu"
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`debug: true
|
||||
security:
|
||||
basic:
|
||||
username: "%s"
|
||||
password-sha512: "%s"
|
||||
password-bcrypt-base64: "%s"
|
||||
endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/health
|
||||
@@ -1202,8 +1209,8 @@ endpoints:
|
||||
if config.Security.Basic.Username != expectedUsername {
|
||||
t.Errorf("config.Security.Basic.Username should've been %s, but was %s", expectedUsername, config.Security.Basic.Username)
|
||||
}
|
||||
if config.Security.Basic.PasswordSha512Hash != expectedPasswordHash {
|
||||
t.Errorf("config.Security.Basic.PasswordSha512Hash should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordSha512Hash)
|
||||
if config.Security.Basic.PasswordBcryptHashBase64Encoded != expectedPasswordHash {
|
||||
t.Errorf("config.Security.Basic.PasswordBcryptHashBase64Encoded should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordBcryptHashBase64Encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,53 +1309,3 @@ endpoints:
|
||||
t.Error("services should've been merged in endpoints")
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Remove this in v4.0.0
|
||||
func TestParseAndValidateConfigBytes_backwardCompatibleWithStorageFile(t *testing.T) {
|
||||
file := t.TempDir() + "/test.db"
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||
storage:
|
||||
type: sqlite
|
||||
file: %s
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/actuator/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`, file)))
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatal("Config shouldn't have been nil")
|
||||
}
|
||||
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite {
|
||||
t.Error("expected storage to be set to sqlite, got", config.Storage)
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Remove this in v4.0.0
|
||||
func TestParseAndValidateConfigBytes_backwardCompatibleWithStorageTypeMemoryAndFile(t *testing.T) {
|
||||
file := t.TempDir() + "/test.db"
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||
storage:
|
||||
type: memory
|
||||
file: %s
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/actuator/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
`, file)))
|
||||
if err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatal("Config shouldn't have been nil")
|
||||
}
|
||||
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeMemory {
|
||||
t.Error("expected storage to be set to memory, got", config.Storage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,15 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "every-day-explicitly-at-2300",
|
||||
cfg: &Config{
|
||||
Start: "23:00",
|
||||
Duration: time.Hour,
|
||||
Every: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "every-monday-at-0000",
|
||||
cfg: &Config{
|
||||
@@ -168,6 +177,24 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-now-for-8h-explicit-days",
|
||||
cfg: &Config{
|
||||
Start: fmt.Sprintf("%02d:00", now.Hour()),
|
||||
Duration: 8 * time.Hour,
|
||||
Every: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-now-for-23h-explicit-days",
|
||||
cfg: &Config{
|
||||
Start: fmt.Sprintf("%02d:00", now.Hour()),
|
||||
Duration: 23 * time.Hour,
|
||||
Every: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-4h-ago-for-8h",
|
||||
cfg: &Config{
|
||||
@@ -176,6 +203,14 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-22h-ago-for-23h",
|
||||
cfg: &Config{
|
||||
Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-22)),
|
||||
Duration: 23 * time.Hour,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-4h-ago-for-3h",
|
||||
cfg: &Config{
|
||||
|
||||
@@ -2,6 +2,7 @@ package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
@@ -16,14 +17,31 @@ var (
|
||||
// StaticFolder is the path to the location of the static folder from the root path of the project
|
||||
// The only reason this is exposed is to allow running tests from a different path than the root path of the project
|
||||
StaticFolder = "./web/static"
|
||||
|
||||
ErrButtonValidationFailed = errors.New("invalid button configuration: missing required name or link")
|
||||
)
|
||||
|
||||
// Config is the configuration for the UI of Gatus
|
||||
type Config struct {
|
||||
Title string `yaml:"title,omitempty"` // Title of the page
|
||||
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
|
||||
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
|
||||
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
|
||||
Title string `yaml:"title,omitempty"` // Title of the page
|
||||
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
|
||||
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
|
||||
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
|
||||
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
|
||||
}
|
||||
|
||||
// Button is the configuration for a button on the UI
|
||||
type Button struct {
|
||||
Name string `yaml:"name,omitempty"` // Name is the text to display on the button
|
||||
Link string `yaml:"link,omitempty"` // Link to open when the button is clicked.
|
||||
}
|
||||
|
||||
// Validate validates the button configuration
|
||||
func (btn *Button) Validate() error {
|
||||
if len(btn.Name) == 0 || len(btn.Link) == 0 {
|
||||
return ErrButtonValidationFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns a Config struct with the default values
|
||||
@@ -47,6 +65,12 @@ func (cfg *Config) ValidateAndSetDefaults() error {
|
||||
if len(cfg.Header) == 0 {
|
||||
cfg.Header = defaultLink
|
||||
}
|
||||
for _, btn := range cfg.Buttons {
|
||||
if err := btn.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Validate that the template works
|
||||
t, err := template.ParseFiles(StaticFolder + "/index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -26,6 +27,45 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestButton_Validate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
Name, Link string
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "",
|
||||
Link: "",
|
||||
ExpectedError: ErrButtonValidationFailed,
|
||||
},
|
||||
{
|
||||
Name: "",
|
||||
Link: "link",
|
||||
ExpectedError: ErrButtonValidationFailed,
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
Link: "",
|
||||
ExpectedError: ErrButtonValidationFailed,
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
Link: "link",
|
||||
ExpectedError: nil,
|
||||
},
|
||||
}
|
||||
for i, scenario := range scenarios {
|
||||
t.Run(strconv.Itoa(i)+"_"+scenario.Name+"_"+scenario.Link, func(t *testing.T) {
|
||||
button := &Button{
|
||||
Name: scenario.Name,
|
||||
Link: scenario.Link,
|
||||
}
|
||||
if err := button.Validate(); err != scenario.ExpectedError {
|
||||
t.Errorf("expected error %v, got %v", scenario.ExpectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultConfig(t *testing.T) {
|
||||
defaultConfig := GetDefaultConfig()
|
||||
if defaultConfig.Title != defaultTitle {
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config/ui"
|
||||
"github.com/TwiN/gatus/v3/config/web"
|
||||
"github.com/TwiN/gatus/v3/controller/handler"
|
||||
"github.com/TwiN/gatus/v3/security"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/config/web"
|
||||
"github.com/TwiN/gatus/v4/controller/handler"
|
||||
"github.com/TwiN/gatus/v4/security"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config"
|
||||
"github.com/TwiN/gatus/v3/config/web"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/config/web"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
func TestHandle(t *testing.T) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -21,6 +22,12 @@ const (
|
||||
badgeColorHexVeryBad = "#c7130a"
|
||||
)
|
||||
|
||||
const (
|
||||
HealthStatusUp = "up"
|
||||
HealthStatusDown = "down"
|
||||
HealthStatusUnknown = "?"
|
||||
)
|
||||
|
||||
// UptimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
//
|
||||
// Valid values for {duration}: 7d, 24h, 1h
|
||||
@@ -95,6 +102,37 @@ func ResponseTimeBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = writer.Write(generateResponseTimeBadgeSVG(duration, averageResponseTime))
|
||||
}
|
||||
|
||||
// HealthBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
func HealthBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
variables := mux.Vars(request)
|
||||
key := variables["key"]
|
||||
pagingConfig := paging.NewEndpointStatusParams()
|
||||
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
healthStatus := HealthStatusUnknown
|
||||
if len(status.Results) > 0 {
|
||||
if status.Results[0].Success {
|
||||
healthStatus = HealthStatusUp
|
||||
} else {
|
||||
healthStatus = HealthStatusDown
|
||||
}
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(generateHealthBadgeSVG(healthStatus))
|
||||
}
|
||||
|
||||
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
||||
var labelWidth, valueWidth, valueWidthAdjustment int
|
||||
switch duration {
|
||||
@@ -223,3 +261,61 @@ func getBadgeColorFromResponseTime(responseTime int) string {
|
||||
}
|
||||
return badgeColorHexVeryBad
|
||||
}
|
||||
|
||||
func generateHealthBadgeSVG(healthStatus string) []byte {
|
||||
var labelWidth, valueWidth int
|
||||
switch healthStatus {
|
||||
case HealthStatusUp:
|
||||
valueWidth = 28
|
||||
case HealthStatusDown:
|
||||
valueWidth = 44
|
||||
case HealthStatusUnknown:
|
||||
valueWidth = 10
|
||||
default:
|
||||
}
|
||||
color := getBadgeColorFromHealth(healthStatus)
|
||||
labelWidth = 48
|
||||
|
||||
width := labelWidth + valueWidth
|
||||
labelX := labelWidth / 2
|
||||
valueX := labelWidth + (valueWidth / 2)
|
||||
svg := []byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
|
||||
<linearGradient id="b" x2="0" y2="100%%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<mask id="a">
|
||||
<rect width="%d" height="20" rx="3" fill="#fff"/>
|
||||
</mask>
|
||||
<g mask="url(#a)">
|
||||
<path fill="#555" d="M0 0h%dv20H0z"/>
|
||||
<path fill="%s" d="M%d 0h%dv20H%dz"/>
|
||||
<path fill="url(#b)" d="M0 0h%dv20H0z"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text x="%d" y="15" fill="#010101" fill-opacity=".3">
|
||||
health
|
||||
</text>
|
||||
<text x="%d" y="14">
|
||||
health
|
||||
</text>
|
||||
<text x="%d" y="15" fill="#010101" fill-opacity=".3">
|
||||
%s
|
||||
</text>
|
||||
<text x="%d" y="14">
|
||||
%s
|
||||
</text>
|
||||
</g>
|
||||
</svg>`, width, width, labelWidth, color, labelWidth, valueWidth, labelWidth, width, labelX, labelX, valueX, healthStatus, valueX, healthStatus))
|
||||
|
||||
return svg
|
||||
}
|
||||
|
||||
func getBadgeColorFromHealth(healthStatus string) string {
|
||||
if healthStatus == HealthStatusUp {
|
||||
return badgeColorHexAwesome
|
||||
} else if healthStatus == HealthStatusDown {
|
||||
return badgeColorHexVeryBad
|
||||
}
|
||||
return badgeColorHexPassable
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/watchdog"
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/watchdog"
|
||||
)
|
||||
|
||||
func TestUptimeBadge(t *testing.T) {
|
||||
func TestBadge(t *testing.T) {
|
||||
defer store.Get().Clear()
|
||||
defer cache.Clear()
|
||||
cfg := &config.Config{
|
||||
@@ -29,8 +29,8 @@ func TestUptimeBadge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.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[0], &core.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()})
|
||||
router := CreateRouter("../../web/static", cfg.Security, nil, cfg.Metrics)
|
||||
type Scenario struct {
|
||||
Name string
|
||||
@@ -89,21 +89,26 @@ func TestUptimeBadge(t *testing.T) {
|
||||
Path: "/api/v1/endpoints/invalid_key/response-times/7d/badge.svg",
|
||||
ExpectedCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
Name: "badge-health-up",
|
||||
Path: "/api/v1/endpoints/core_frontend/health/badge.svg",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "badge-health-down",
|
||||
Path: "/api/v1/endpoints/core_backend/health/badge.svg",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "badge-health-for-invalid-key",
|
||||
Path: "/api/v1/endpoints/invalid_key/health/badge.svg",
|
||||
ExpectedCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
Name: "chart-response-time-24h",
|
||||
Path: "/api/v1/endpoints/core_backend/response-times/24h/chart.svg",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{ // XXX: Remove this in v4.0.0
|
||||
Name: "backward-compatible-services-badge-uptime-1h",
|
||||
Path: "/api/v1/services/core_frontend/uptimes/1h/badge.svg",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{ // XXX: Remove this in v4.0.0
|
||||
Name: "backward-compatible-services-chart-response-time-24h",
|
||||
Path: "/api/v1/services/core_backend/response-times/24h/chart.svg",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
@@ -229,3 +234,30 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBadgeColorFromHealth(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
HealthStatus string
|
||||
ExpectedColor string
|
||||
}{
|
||||
{
|
||||
HealthStatus: HealthStatusUp,
|
||||
ExpectedColor: badgeColorHexAwesome,
|
||||
},
|
||||
{
|
||||
HealthStatus: HealthStatusDown,
|
||||
ExpectedColor: badgeColorHexVeryBad,
|
||||
},
|
||||
{
|
||||
HealthStatus: HealthStatusUnknown,
|
||||
ExpectedColor: badgeColorHexPassable,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run("health-"+scenario.HealthStatus, func(t *testing.T) {
|
||||
if getBadgeColorFromHealth(scenario.HealthStatus) != scenario.ExpectedColor {
|
||||
t.Errorf("expected %s from %s, got %v", scenario.ExpectedColor, scenario.HealthStatus, getBadgeColorFromHealth(scenario.HealthStatus))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/watchdog"
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/watchdog"
|
||||
)
|
||||
|
||||
func TestResponseTimeChart(t *testing.T) {
|
||||
@@ -58,11 +58,6 @@ func TestResponseTimeChart(t *testing.T) {
|
||||
Path: "/api/v1/endpoints/invalid_key/response-times/7d/chart.svg",
|
||||
ExpectedCode: http.StatusNotFound,
|
||||
},
|
||||
{ // XXX: Remove this in v4.0.0
|
||||
Name: "backward-compatible-services-chart-response-time-24h",
|
||||
Path: "/api/v1/services/core_backend/response-times/24h/chart.svg",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/security"
|
||||
"github.com/TwiN/gatus/v4/security"
|
||||
)
|
||||
|
||||
// ConfigHandler is a handler that returns information for the front end of the application.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/security"
|
||||
"github.com/TwiN/gatus/v4/security"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
||||
"github.com/TwiN/gocache"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
"github.com/TwiN/gocache/v2"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,17 +6,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/watchdog"
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/watchdog"
|
||||
)
|
||||
|
||||
var (
|
||||
firstCondition = core.Condition("[STATUS] == 200")
|
||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
|
||||
timestamp = time.Now()
|
||||
|
||||
testEndpoint = core.Endpoint{
|
||||
@@ -26,7 +22,7 @@ var (
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
|
||||
Conditions: []core.Condition{core.Condition("[STATUS] == 200"), core.Condition("[RESPONSE_TIME] < 500"), core.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
@@ -131,11 +127,6 @@ func TestEndpointStatus(t *testing.T) {
|
||||
Path: "/api/v1/endpoints/invalid_key/statuses",
|
||||
ExpectedCode: http.StatusNotFound,
|
||||
},
|
||||
{ // XXX: Remove this in v4.0.0
|
||||
Name: "backward-compatible-service-status",
|
||||
Path: "/api/v1/services/core_frontend/statuses",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
@@ -201,12 +192,6 @@ func TestEndpointStatuses(t *testing.T) {
|
||||
ExpectedCode: http.StatusOK,
|
||||
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`,
|
||||
},
|
||||
{ // XXX: Remove this in v4.0.0
|
||||
Name: "backward-compatible-service-status",
|
||||
Path: "/api/v1/services/statuses",
|
||||
ExpectedCode: http.StatusOK,
|
||||
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
@@ -3,8 +3,8 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config/ui"
|
||||
"github.com/TwiN/gatus/v3/security"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
"github.com/TwiN/gatus/v4/security"
|
||||
"github.com/TwiN/health"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -30,21 +30,14 @@ func CreateRouter(staticFolder string, securityConfig *security.Config, uiConfig
|
||||
unprotected.Handle("/v1/config", ConfigHandler{securityConfig: securityConfig}).Methods("GET")
|
||||
protected.HandleFunc("/v1/endpoints/statuses", EndpointStatuses).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already
|
||||
protected.HandleFunc("/v1/endpoints/{key}/statuses", GzipHandlerFunc(EndpointStatus)).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/health/badge.svg", HealthBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/uptimes/{duration}/badge.svg", UptimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).Methods("GET")
|
||||
// XXX: Remove the lines between this and the next XXX comment in v4.0.0
|
||||
protected.HandleFunc("/v1/services/statuses", EndpointStatuses).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already
|
||||
protected.HandleFunc("/v1/services/{key}/statuses", GzipHandlerFunc(EndpointStatus)).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/services/{key}/uptimes/{duration}/badge.svg", UptimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/services/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/services/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).Methods("GET")
|
||||
// XXX: Remove the lines between this and the previous XXX comment in v4.0.0
|
||||
// Misc
|
||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||
router.HandleFunc("/favicon.ico", FavIcon(staticFolder)).Methods("GET")
|
||||
// SPA
|
||||
router.HandleFunc("/services/{name}", SinglePageApplication(staticFolder, uiConfig)).Methods("GET") // XXX: Remove this in v4.0.0
|
||||
router.HandleFunc("/endpoints/{name}", SinglePageApplication(staticFolder, uiConfig)).Methods("GET")
|
||||
router.HandleFunc("/", SinglePageApplication(staticFolder, uiConfig)).Methods("GET")
|
||||
// Everything else falls back on static content
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config/ui"
|
||||
"github.com/TwiN/gatus/v4/config/ui"
|
||||
)
|
||||
|
||||
func SinglePageApplication(staticFolder string, ui *ui.Config) http.HandlerFunc {
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/watchdog"
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/watchdog"
|
||||
)
|
||||
|
||||
func TestSinglePageApplication(t *testing.T) {
|
||||
@@ -48,11 +48,6 @@ func TestSinglePageApplication(t *testing.T) {
|
||||
Path: "/endpoints/core_frontend",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{ // XXX: Remove this in v4.0.0
|
||||
Name: "frontend-service",
|
||||
Path: "/services/core_frontend",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/jsonpath"
|
||||
"github.com/TwiN/gatus/v3/pattern"
|
||||
"github.com/TwiN/gatus/v4/jsonpath"
|
||||
"github.com/TwiN/gatus/v4/pattern"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -2,8 +2,9 @@ package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/pattern"
|
||||
"github.com/TwiN/gatus/v4/pattern"
|
||||
)
|
||||
|
||||
func TestIntegrationQuery(t *testing.T) {
|
||||
@@ -21,7 +22,7 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
QueryType: "A",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
inputURL: "8.8.8.8",
|
||||
inputURL: "1.1.1.1",
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: "93.184.216.34",
|
||||
},
|
||||
@@ -31,7 +32,7 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
QueryType: "AAAA",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
inputURL: "8.8.8.8",
|
||||
inputURL: "1.1.1.1",
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: "2606:2800:220:1:248:1893:25c8:1946",
|
||||
},
|
||||
@@ -39,11 +40,11 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
name: "test DNS with type CNAME",
|
||||
inputDNS: DNS{
|
||||
QueryType: "CNAME",
|
||||
QueryName: "doc.google.com.",
|
||||
QueryName: "en.wikipedia.org.",
|
||||
},
|
||||
inputURL: "8.8.8.8",
|
||||
inputURL: "1.1.1.1",
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: "writely.l.google.com.",
|
||||
expectedBody: "dyna.wikimedia.org.",
|
||||
},
|
||||
{
|
||||
name: "test DNS with type MX",
|
||||
@@ -51,7 +52,7 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
QueryType: "MX",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
inputURL: "8.8.8.8",
|
||||
inputURL: "1.1.1.1",
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: ".",
|
||||
},
|
||||
@@ -61,7 +62,7 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
QueryType: "NS",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
inputURL: "8.8.8.8",
|
||||
inputURL: "1.1.1.1",
|
||||
expectedDNSCode: "NOERROR",
|
||||
expectedBody: "*.iana-servers.net.",
|
||||
},
|
||||
@@ -69,15 +70,14 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
name: "test DNS with fake type and retrieve error",
|
||||
inputDNS: DNS{
|
||||
QueryType: "B",
|
||||
QueryName: "google",
|
||||
QueryName: "example",
|
||||
},
|
||||
inputURL: "8.8.8.8",
|
||||
inputURL: "1.1.1.1",
|
||||
isErrExpected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dns := test.inputDNS
|
||||
result := &Result{}
|
||||
@@ -86,9 +86,8 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
t.Errorf("there should be errors")
|
||||
}
|
||||
if result.DNSRCode != test.expectedDNSCode {
|
||||
t.Errorf("DNSRCodePlaceholder '%s' should have been %s", result.DNSRCode, test.expectedDNSCode)
|
||||
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, result.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(result.body)) {
|
||||
@@ -100,6 +99,7 @@ func TestIntegrationQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
})
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core/ui"
|
||||
"github.com/TwiN/gatus/v3/util"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core/ui"
|
||||
"github.com/TwiN/gatus/v4/util"
|
||||
)
|
||||
|
||||
type EndpointType string
|
||||
|
||||
const (
|
||||
// HostHeader is the name of the header used to specify the host
|
||||
HostHeader = "Host"
|
||||
@@ -30,6 +32,14 @@ const (
|
||||
|
||||
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
||||
GatusUserAgent = "Gatus/1.0"
|
||||
|
||||
// EndpointType enum for the endpoint type.
|
||||
EndpointTypeDNS EndpointType = "DNS"
|
||||
EndpointTypeTCP EndpointType = "TCP"
|
||||
EndpointTypeICMP EndpointType = "ICMP"
|
||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
||||
EndpointTypeTLS EndpointType = "TLS"
|
||||
EndpointTypeHTTP EndpointType = "HTTP"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -79,7 +89,7 @@ type Endpoint struct {
|
||||
Interval time.Duration `yaml:"interval,omitempty"`
|
||||
|
||||
// Conditions used to determine the health of the endpoint
|
||||
Conditions []*Condition `yaml:"conditions"`
|
||||
Conditions []Condition `yaml:"conditions"`
|
||||
|
||||
// Alerts is the alerting configuration for the endpoint in case of failure
|
||||
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
||||
@@ -105,13 +115,33 @@ func (endpoint Endpoint) IsEnabled() bool {
|
||||
return *endpoint.Enabled
|
||||
}
|
||||
|
||||
// Type returns the endpoint type
|
||||
func (endpoint Endpoint) Type() EndpointType {
|
||||
switch {
|
||||
case endpoint.DNS != nil:
|
||||
return EndpointTypeDNS
|
||||
case strings.HasPrefix(endpoint.URL, "tcp://"):
|
||||
return EndpointTypeTCP
|
||||
case strings.HasPrefix(endpoint.URL, "icmp://"):
|
||||
return EndpointTypeICMP
|
||||
case strings.HasPrefix(endpoint.URL, "starttls://"):
|
||||
return EndpointTypeSTARTTLS
|
||||
case strings.HasPrefix(endpoint.URL, "tls://"):
|
||||
return EndpointTypeTLS
|
||||
default:
|
||||
return EndpointTypeHTTP
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of fields that have one
|
||||
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
||||
// Set default values
|
||||
if endpoint.ClientConfig == nil {
|
||||
endpoint.ClientConfig = client.GetDefaultConfig()
|
||||
} else {
|
||||
endpoint.ClientConfig.ValidateAndSetDefaults()
|
||||
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if endpoint.UIConfig == nil {
|
||||
endpoint.UIConfig = ui.GetDefaultConfig()
|
||||
@@ -194,7 +224,15 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
||||
// No need to keep the body after the endpoint has been evaluated
|
||||
result.body = nil
|
||||
// Clean up parameters that we don't need to keep in the results
|
||||
if endpoint.UIConfig.HideURL {
|
||||
for errIdx, errorString := range result.Errors {
|
||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, endpoint.URL, "<redacted>")
|
||||
}
|
||||
}
|
||||
if endpoint.UIConfig.HideHostname {
|
||||
for errIdx, errorString := range result.Errors {
|
||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
||||
}
|
||||
result.Hostname = ""
|
||||
}
|
||||
return result
|
||||
@@ -224,21 +262,16 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
var response *http.Response
|
||||
var err error
|
||||
var certificate *x509.Certificate
|
||||
isTypeDNS := endpoint.DNS != nil
|
||||
isTypeTCP := strings.HasPrefix(endpoint.URL, "tcp://")
|
||||
isTypeICMP := strings.HasPrefix(endpoint.URL, "icmp://")
|
||||
isTypeSTARTTLS := strings.HasPrefix(endpoint.URL, "starttls://")
|
||||
isTypeTLS := strings.HasPrefix(endpoint.URL, "tls://")
|
||||
isTypeHTTP := !isTypeDNS && !isTypeTCP && !isTypeICMP && !isTypeSTARTTLS && !isTypeTLS
|
||||
if isTypeHTTP {
|
||||
endpointType := endpoint.Type()
|
||||
if endpointType == EndpointTypeHTTP {
|
||||
request = endpoint.buildHTTPRequest()
|
||||
}
|
||||
startTime := time.Now()
|
||||
if isTypeDNS {
|
||||
if endpointType == EndpointTypeDNS {
|
||||
endpoint.DNS.query(endpoint.URL, result)
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if isTypeSTARTTLS || isTypeTLS {
|
||||
if isTypeSTARTTLS {
|
||||
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS {
|
||||
if endpointType == EndpointTypeSTARTTLS {
|
||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
||||
} else {
|
||||
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig)
|
||||
@@ -249,10 +282,10 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||
} else if isTypeTCP {
|
||||
} else if endpointType == EndpointTypeTCP {
|
||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if isTypeICMP {
|
||||
} else if endpointType == EndpointTypeICMP {
|
||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
||||
} else {
|
||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package core
|
||||
|
||||
import "github.com/TwiN/gatus/v3/util"
|
||||
import "github.com/TwiN/gatus/v4/util"
|
||||
|
||||
// EndpointStatus contains the evaluation Results of an Endpoint
|
||||
type EndpointStatus struct {
|
||||
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||
"github.com/TwiN/gatus/v4/client"
|
||||
"github.com/TwiN/gatus/v4/core/ui"
|
||||
)
|
||||
|
||||
func TestEndpoint_IsEnabled(t *testing.T) {
|
||||
@@ -22,12 +23,67 @@ func TestEndpoint_IsEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_Type(t *testing.T) {
|
||||
type fields struct {
|
||||
URL string
|
||||
DNS *DNS
|
||||
}
|
||||
tests := []struct {
|
||||
fields fields
|
||||
want EndpointType
|
||||
}{{
|
||||
fields: fields{
|
||||
URL: "1.1.1.1",
|
||||
DNS: &DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com",
|
||||
},
|
||||
},
|
||||
want: EndpointTypeDNS,
|
||||
}, {
|
||||
fields: fields{
|
||||
URL: "tcp://127.0.0.1:6379",
|
||||
},
|
||||
want: EndpointTypeTCP,
|
||||
}, {
|
||||
fields: fields{
|
||||
URL: "icmp://example.com",
|
||||
},
|
||||
want: EndpointTypeICMP,
|
||||
}, {
|
||||
fields: fields{
|
||||
URL: "starttls://smtp.gmail.com:587",
|
||||
},
|
||||
want: EndpointTypeSTARTTLS,
|
||||
}, {
|
||||
fields: fields{
|
||||
URL: "tls://example.com:443",
|
||||
},
|
||||
want: EndpointTypeTLS,
|
||||
}, {
|
||||
fields: fields{
|
||||
URL: "https://twin.sh/health",
|
||||
},
|
||||
want: EndpointTypeHTTP,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.want), func(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
URL: tt.fields.URL,
|
||||
DNS: tt.fields.DNS,
|
||||
}
|
||||
if got := endpoint.Type(); got != tt.want {
|
||||
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_ValidateAndSetDefaults(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
endpoint := Endpoint{
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{Condition("[STATUS] == 200")},
|
||||
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
@@ -72,7 +128,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
ClientConfig: &client.Config{
|
||||
Insecure: true,
|
||||
IgnoreRedirect: true,
|
||||
@@ -101,7 +157,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithNoName(t *testing.T) {
|
||||
endpoint := &Endpoint{
|
||||
Name: "",
|
||||
URL: "http://example.com",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
}
|
||||
err := endpoint.ValidateAndSetDefaults()
|
||||
if err == nil {
|
||||
@@ -115,7 +171,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithNoUrl(t *testing.T) {
|
||||
endpoint := &Endpoint{
|
||||
Name: "example",
|
||||
URL: "",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
}
|
||||
err := endpoint.ValidateAndSetDefaults()
|
||||
if err == nil {
|
||||
@@ -137,7 +193,6 @@ func TestEndpoint_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
conditionSuccess := Condition("[DNS_RCODE] == NOERROR")
|
||||
endpoint := &Endpoint{
|
||||
Name: "dns-test",
|
||||
URL: "http://example.com",
|
||||
@@ -145,7 +200,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
QueryType: "A",
|
||||
QueryName: "example.com",
|
||||
},
|
||||
Conditions: []*Condition{&conditionSuccess},
|
||||
Conditions: []Condition{Condition("[DNS_RCODE] == NOERROR")},
|
||||
}
|
||||
err := endpoint.ValidateAndSetDefaults()
|
||||
if err != nil {
|
||||
@@ -161,7 +216,7 @@ func TestEndpoint_buildHTTPRequest(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
request := endpoint.buildHTTPRequest()
|
||||
@@ -181,7 +236,7 @@ func TestEndpoint_buildHTTPRequestWithCustomUserAgent(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
Headers: map[string]string{
|
||||
"User-Agent": "Test/2.0",
|
||||
},
|
||||
@@ -205,7 +260,7 @@ func TestEndpoint_buildHTTPRequestWithHostHeader(t *testing.T) {
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Method: "POST",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
Headers: map[string]string{
|
||||
"Host": "example.com",
|
||||
},
|
||||
@@ -226,7 +281,7 @@ func TestEndpoint_buildHTTPRequestWithGraphQLEnabled(t *testing.T) {
|
||||
Name: "website-graphql",
|
||||
URL: "https://twin.sh/graphql",
|
||||
Method: "POST",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
GraphQL: true,
|
||||
Body: `{
|
||||
users(gender: "female") {
|
||||
@@ -257,7 +312,7 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []*Condition{&condition, &bodyCondition},
|
||||
Conditions: []Condition{condition, bodyCondition},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
result := endpoint.EvaluateHealth()
|
||||
@@ -270,6 +325,9 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
||||
if !result.Success {
|
||||
t.Error("Because all conditions passed, this should have been a success")
|
||||
}
|
||||
if result.Hostname != "twin.sh" {
|
||||
t.Error("result.Hostname should've been twin.sh, but was", result.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
|
||||
@@ -277,7 +335,7 @@ func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "website-health",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []*Condition{&condition},
|
||||
Conditions: []Condition{condition},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
result := endpoint.EvaluateHealth()
|
||||
@@ -288,7 +346,78 @@ func TestIntegrationEvaluateHealthWithFailure(t *testing.T) {
|
||||
t.Error("Because the connection has been established, result.Connected should've been true")
|
||||
}
|
||||
if result.Success {
|
||||
t.Error("Because one of the conditions failed, success should have been false")
|
||||
t.Error("Because one of the conditions failed, result.Success should have been false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthWithInvalidCondition(t *testing.T) {
|
||||
condition := Condition("[STATUS] invalid 200")
|
||||
endpoint := Endpoint{
|
||||
Name: "invalid-condition",
|
||||
URL: "https://twin.sh/health",
|
||||
Conditions: []Condition{condition},
|
||||
}
|
||||
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
||||
// XXX: Should this really not return an error? After all, the condition is not valid and conditions are part of the endpoint...
|
||||
t.Error("endpoint validation should've been successful, but wasn't")
|
||||
}
|
||||
result := endpoint.EvaluateHealth()
|
||||
if result.Success {
|
||||
t.Error("Because one of the conditions was invalid, result.Success should have been false")
|
||||
}
|
||||
if len(result.Errors) == 0 {
|
||||
t.Error("There should've been an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthWithError(t *testing.T) {
|
||||
condition := Condition("[STATUS] == 200")
|
||||
endpoint := Endpoint{
|
||||
Name: "invalid-host",
|
||||
URL: "http://invalid/health",
|
||||
Conditions: []Condition{condition},
|
||||
UIConfig: &ui.Config{
|
||||
HideHostname: true,
|
||||
},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
result := endpoint.EvaluateHealth()
|
||||
if result.Success {
|
||||
t.Error("Because one of the conditions was invalid, result.Success should have been false")
|
||||
}
|
||||
if len(result.Errors) == 0 {
|
||||
t.Error("There should've been an error")
|
||||
}
|
||||
if !strings.Contains(result.Errors[0], "<redacted>") {
|
||||
t.Error("result.Errors[0] should've had the hostname redacted because ui.hide-hostname is set to true")
|
||||
}
|
||||
if result.Hostname != "" {
|
||||
t.Error("result.Hostname should've been empty because ui.hide-hostname is set to true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthWithErrorAndHideURL(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "invalid-url",
|
||||
URL: "https://httpstat.us/200?sleep=100",
|
||||
Conditions: []Condition{Condition("[STATUS] == 200")},
|
||||
ClientConfig: &client.Config{
|
||||
Timeout: 1 * time.Millisecond,
|
||||
},
|
||||
UIConfig: &ui.Config{
|
||||
HideURL: true,
|
||||
},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
result := endpoint.EvaluateHealth()
|
||||
if result.Success {
|
||||
t.Error("Because one of the conditions was invalid, result.Success should have been false")
|
||||
}
|
||||
if len(result.Errors) == 0 {
|
||||
t.Error("There should've been an error")
|
||||
}
|
||||
if !strings.Contains(result.Errors[0], "<redacted>") || strings.Contains(result.Errors[0], endpoint.URL) {
|
||||
t.Error("result.Errors[0] should've had the URL redacted because ui.hide-url is set to true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,12 +426,12 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||
conditionBody := Condition("[BODY] == 93.184.216.34")
|
||||
endpoint := Endpoint{
|
||||
Name: "example",
|
||||
URL: "8.8.8.8",
|
||||
URL: "1.1.1.1",
|
||||
DNS: &DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com.",
|
||||
},
|
||||
Conditions: []*Condition{&conditionSuccess, &conditionBody},
|
||||
Conditions: []Condition{conditionSuccess, conditionBody},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
result := endpoint.EvaluateHealth()
|
||||
@@ -322,7 +451,7 @@ func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "icmp-test",
|
||||
URL: "icmp://127.0.0.1",
|
||||
Conditions: []*Condition{&conditionSuccess},
|
||||
Conditions: []Condition{conditionSuccess},
|
||||
}
|
||||
endpoint.ValidateAndSetDefaults()
|
||||
result := endpoint.EvaluateHealth()
|
||||
@@ -342,7 +471,7 @@ func TestEndpoint_getIP(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "invalid-url-test",
|
||||
URL: "",
|
||||
Conditions: []*Condition{&conditionSuccess},
|
||||
Conditions: []Condition{conditionSuccess},
|
||||
}
|
||||
result := &Result{}
|
||||
endpoint.getIP(result)
|
||||
@@ -355,22 +484,22 @@ func TestEndpoint_NeedsToReadBody(t *testing.T) {
|
||||
statusCondition := Condition("[STATUS] == 200")
|
||||
bodyCondition := Condition("[BODY].status == UP")
|
||||
bodyConditionWithLength := Condition("len([BODY].tags) > 0")
|
||||
if (&Endpoint{Conditions: []*Condition{&statusCondition}}).needsToReadBody() {
|
||||
if (&Endpoint{Conditions: []Condition{statusCondition}}).needsToReadBody() {
|
||||
t.Error("expected false, got true")
|
||||
}
|
||||
if !(&Endpoint{Conditions: []*Condition{&bodyCondition}}).needsToReadBody() {
|
||||
if !(&Endpoint{Conditions: []Condition{bodyCondition}}).needsToReadBody() {
|
||||
t.Error("expected true, got false")
|
||||
}
|
||||
if !(&Endpoint{Conditions: []*Condition{&bodyConditionWithLength}}).needsToReadBody() {
|
||||
if !(&Endpoint{Conditions: []Condition{bodyConditionWithLength}}).needsToReadBody() {
|
||||
t.Error("expected true, got false")
|
||||
}
|
||||
if !(&Endpoint{Conditions: []*Condition{&statusCondition, &bodyCondition}}).needsToReadBody() {
|
||||
if !(&Endpoint{Conditions: []Condition{statusCondition, bodyCondition}}).needsToReadBody() {
|
||||
t.Error("expected true, got false")
|
||||
}
|
||||
if !(&Endpoint{Conditions: []*Condition{&bodyCondition, &statusCondition}}).needsToReadBody() {
|
||||
if !(&Endpoint{Conditions: []Condition{bodyCondition, statusCondition}}).needsToReadBody() {
|
||||
t.Error("expected true, got false")
|
||||
}
|
||||
if !(&Endpoint{Conditions: []*Condition{&bodyConditionWithLength, &statusCondition}}).needsToReadBody() {
|
||||
if !(&Endpoint{Conditions: []Condition{bodyConditionWithLength, statusCondition}}).needsToReadBody() {
|
||||
t.Error("expected true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ type Result struct {
|
||||
HTTPStatus int `json:"status"`
|
||||
|
||||
// DNSRCode is the response code of a DNS query in a human-readable format
|
||||
//
|
||||
// Possible values: NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED
|
||||
DNSRCode string `json:"-"`
|
||||
|
||||
// Hostname extracted from Endpoint.URL
|
||||
|
||||
@@ -4,6 +4,8 @@ package ui
|
||||
type Config struct {
|
||||
// HideHostname whether to hide the hostname in the Result
|
||||
HideHostname bool `yaml:"hide-hostname"`
|
||||
// HideURL whether to ensure the URL is not displayed in the results. Useful if the URL contains a token.
|
||||
HideURL bool `yaml:"hide-url"`
|
||||
// DontResolveFailedConditions whether to resolve failed conditions in the Result for display in the UI
|
||||
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
|
||||
}
|
||||
@@ -12,6 +14,7 @@ type Config struct {
|
||||
func GetDefaultConfig() *Config {
|
||||
return &Config{
|
||||
HideHostname: false,
|
||||
HideURL: false,
|
||||
DontResolveFailedConditions: false,
|
||||
}
|
||||
}
|
||||
|
||||
8
go.mod
8
go.mod
@@ -1,12 +1,11 @@
|
||||
module github.com/TwiN/gatus/v3
|
||||
module github.com/TwiN/gatus/v4
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/TwiN/g8 v1.3.0
|
||||
github.com/TwiN/gocache v1.2.4
|
||||
github.com/TwiN/gocache/v2 v2.0.0
|
||||
github.com/TwiN/health v1.3.0
|
||||
github.com/TwiN/health v1.4.0
|
||||
github.com/coreos/go-oidc/v3 v3.1.0
|
||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
|
||||
github.com/google/uuid v1.3.0
|
||||
@@ -34,7 +33,6 @@ require (
|
||||
github.com/prometheus/common v0.31.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
|
||||
|
||||
35
go.sum
35
go.sum
@@ -35,12 +35,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/TwiN/g8 v1.3.0 h1:mNv3R35GhDn1gEV0BKMl1oupZ1tDtOWPTHUKu+W/k3U=
|
||||
github.com/TwiN/g8 v1.3.0/go.mod h1:SiIdItS0agSUloFqdQQt/RObB2jGSq+nnE9WfFv3RIo=
|
||||
github.com/TwiN/gocache v1.2.4 h1:AfJ1YRcxtQ/zZEN61URDwk/dwFG7LSRenU5qIm9dQzo=
|
||||
github.com/TwiN/gocache v1.2.4/go.mod h1:BjabsQQy6z5uHDorHa4LJVPEzFeitLIDbCtdv3gc1gA=
|
||||
github.com/TwiN/gocache/v2 v2.0.0 h1:CPbDNKdSJpmBkh7aWcO7D3KK1yWaMlwX+3dsBPE8/so=
|
||||
github.com/TwiN/gocache/v2 v2.0.0/go.mod h1:j4MABVaia2Tp53ERWc/3l4YxkswtPjB2hQzmL/kD/VQ=
|
||||
github.com/TwiN/health v1.3.0 h1:xw90rZqg0NH5MRkVHzlgtDdP+EQd43v3yMqQVtYlGHg=
|
||||
github.com/TwiN/health v1.3.0/go.mod h1:Bt+lEvSi6C/9NWb7OoGmUmgtS4dfPeMM9EINnURv5dE=
|
||||
github.com/TwiN/health v1.4.0 h1:Ts7lb4ihYDpVEbFSGAhSEZTSwuDOADnwJLFngFl4xzw=
|
||||
github.com/TwiN/health v1.4.0/go.mod h1:CSUh+ryfD2POS2vKtc/yO4IxgR58lKvQ0/8qnoPqPqs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -70,8 +68,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -83,7 +79,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871 h1:wtjTfjwAR/BYYMJ+QOLI/3J/qGEI0fgrkZvgsEWK2/Q=
|
||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
@@ -149,7 +144,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -187,13 +181,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -233,17 +220,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/tidwall/redcon v1.3.2/go.mod h1:bdYBm4rlcWpst2XMwKVzWDF9CoUxEbUmM7CQrKeOZas=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -289,12 +271,10 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -318,7 +298,6 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
@@ -327,7 +306,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -351,7 +329,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -362,10 +339,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -381,12 +355,10 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -396,7 +368,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c h1:EyJTLQbOxvk8V6oDdD8ILR1BOs3nEJXThD6aqsiPNkM=
|
||||
golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -544,12 +515,10 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
8
main.go
8
main.go
@@ -7,10 +7,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/config"
|
||||
"github.com/TwiN/gatus/v3/controller"
|
||||
"github.com/TwiN/gatus/v3/storage/store"
|
||||
"github.com/TwiN/gatus/v3/watchdog"
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/controller"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/watchdog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
// This will be initialized once PublishMetricsForEndpoint.
|
||||
// The reason why we're doing this is that if metrics are disabled, we don't want to initialize it unnecessarily.
|
||||
resultCount *prometheus.CounterVec = nil
|
||||
)
|
||||
|
||||
// PublishMetricsForEndpoint publishes metrics for the given endpoint and its result.
|
||||
// These metrics will be exposed at /metrics if the metrics are enabled
|
||||
func PublishMetricsForEndpoint(endpoint *core.Endpoint, result *core.Result) {
|
||||
if resultCount == nil {
|
||||
resultCount = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "gatus_results_total",
|
||||
Help: "Number of results per endpoint",
|
||||
}, []string{"key", "group", "name", "success"})
|
||||
}
|
||||
resultCount.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, strconv.FormatBool(result.Success)).Inc()
|
||||
}
|
||||
73
metrics/metrics.go
Normal file
73
metrics/metrics.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
const namespace = "gatus" // The prefix of the metrics
|
||||
|
||||
var (
|
||||
initializedMetrics bool // Whether the metrics have been initialized
|
||||
|
||||
resultTotal *prometheus.CounterVec
|
||||
resultDurationSeconds *prometheus.GaugeVec
|
||||
resultConnectedTotal *prometheus.CounterVec
|
||||
resultCodeTotal *prometheus.CounterVec
|
||||
resultCertificateExpirationSeconds *prometheus.GaugeVec
|
||||
)
|
||||
|
||||
func initializePrometheusMetrics() {
|
||||
resultTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: "results_total",
|
||||
Help: "Number of results per endpoint",
|
||||
}, []string{"key", "group", "name", "type", "success"})
|
||||
resultDurationSeconds = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "results_duration_seconds",
|
||||
Help: "Duration of the request in seconds",
|
||||
}, []string{"key", "group", "name", "type"})
|
||||
resultConnectedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: "results_connected_total",
|
||||
Help: "Total number of results in which a connection was successfully established",
|
||||
}, []string{"key", "group", "name", "type"})
|
||||
resultCodeTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: "results_code_total",
|
||||
Help: "Total number of results by code",
|
||||
}, []string{"key", "group", "name", "type", "code"})
|
||||
resultCertificateExpirationSeconds = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "results_certificate_expiration_seconds",
|
||||
Help: "Number of seconds until the certificate expires",
|
||||
}, []string{"key", "group", "name", "type"})
|
||||
}
|
||||
|
||||
// PublishMetricsForEndpoint publishes metrics for the given endpoint and its result.
|
||||
// These metrics will be exposed at /metrics if the metrics are enabled
|
||||
func PublishMetricsForEndpoint(endpoint *core.Endpoint, result *core.Result) {
|
||||
if !initializedMetrics {
|
||||
initializePrometheusMetrics()
|
||||
initializedMetrics = true
|
||||
}
|
||||
endpointType := endpoint.Type()
|
||||
resultTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.FormatBool(result.Success)).Inc()
|
||||
resultDurationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.Duration.Seconds())
|
||||
if result.Connected {
|
||||
resultConnectedTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Inc()
|
||||
}
|
||||
if result.DNSRCode != "" {
|
||||
resultCodeTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), result.DNSRCode).Inc()
|
||||
}
|
||||
if result.HTTPStatus != 0 {
|
||||
resultCodeTotal.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType), strconv.Itoa(result.HTTPStatus)).Inc()
|
||||
}
|
||||
if result.CertificateExpiration != 0 {
|
||||
resultCertificateExpirationSeconds.WithLabelValues(endpoint.Key(), endpoint.Group, endpoint.Name, string(endpointType)).Set(result.CertificateExpiration.Seconds())
|
||||
}
|
||||
}
|
||||
116
metrics/metrics_test.go
Normal file
116
metrics/metrics_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
)
|
||||
|
||||
func TestPublishMetricsForEndpoint(t *testing.T) {
|
||||
httpEndpoint := &core.Endpoint{Name: "http-ep-name", Group: "http-ep-group", URL: "https://example.org"}
|
||||
PublishMetricsForEndpoint(httpEndpoint, &core.Result{
|
||||
HTTPStatus: 200,
|
||||
Connected: true,
|
||||
Duration: 123 * time.Millisecond,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[STATUS] == 200", Success: true},
|
||||
{Condition: "[CERTIFICATE_EXPIRATION] > 48h", Success: true},
|
||||
},
|
||||
Success: true,
|
||||
CertificateExpiration: 49 * time.Hour,
|
||||
})
|
||||
err := testutil.GatherAndCompare(prometheus.Gatherers{prometheus.DefaultGatherer}, bytes.NewBufferString(`
|
||||
# HELP gatus_results_certificate_expiration_seconds Number of seconds until the certificate expires
|
||||
# TYPE gatus_results_certificate_expiration_seconds gauge
|
||||
gatus_results_certificate_expiration_seconds{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 176400
|
||||
# HELP gatus_results_code_total Total number of results by code
|
||||
# TYPE gatus_results_code_total counter
|
||||
gatus_results_code_total{code="200",group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 1
|
||||
# HELP gatus_results_connected_total Total number of results in which a connection was successfully established
|
||||
# TYPE gatus_results_connected_total counter
|
||||
gatus_results_connected_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 1
|
||||
# HELP gatus_results_duration_seconds Duration of the request in seconds
|
||||
# TYPE gatus_results_duration_seconds gauge
|
||||
gatus_results_duration_seconds{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 0.123
|
||||
# HELP gatus_results_total Number of results per endpoint
|
||||
# TYPE gatus_results_total counter
|
||||
gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",success="true",type="HTTP"} 1
|
||||
`), "gatus_results_code_total", "gatus_results_connected_total", "gatus_results_duration_seconds", "gatus_results_total", "gatus_results_certificate_expiration_seconds")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
PublishMetricsForEndpoint(httpEndpoint, &core.Result{
|
||||
HTTPStatus: 200,
|
||||
Connected: true,
|
||||
Duration: 125 * time.Millisecond,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[STATUS] == 200", Success: true},
|
||||
{Condition: "[CERTIFICATE_EXPIRATION] > 47h", Success: false},
|
||||
},
|
||||
Success: false,
|
||||
CertificateExpiration: 47 * time.Hour,
|
||||
})
|
||||
err = testutil.GatherAndCompare(prometheus.Gatherers{prometheus.DefaultGatherer}, bytes.NewBufferString(`
|
||||
# HELP gatus_results_certificate_expiration_seconds Number of seconds until the certificate expires
|
||||
# TYPE gatus_results_certificate_expiration_seconds gauge
|
||||
gatus_results_certificate_expiration_seconds{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 169200
|
||||
# HELP gatus_results_code_total Total number of results by code
|
||||
# TYPE gatus_results_code_total counter
|
||||
gatus_results_code_total{code="200",group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 2
|
||||
# HELP gatus_results_connected_total Total number of results in which a connection was successfully established
|
||||
# TYPE gatus_results_connected_total counter
|
||||
gatus_results_connected_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 2
|
||||
# HELP gatus_results_duration_seconds Duration of the request in seconds
|
||||
# TYPE gatus_results_duration_seconds gauge
|
||||
gatus_results_duration_seconds{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 0.125
|
||||
# HELP gatus_results_total Number of results per endpoint
|
||||
# TYPE gatus_results_total counter
|
||||
gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",success="false",type="HTTP"} 1
|
||||
gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",success="true",type="HTTP"} 1
|
||||
`), "gatus_results_code_total", "gatus_results_connected_total", "gatus_results_duration_seconds", "gatus_results_total", "gatus_results_certificate_expiration_seconds")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
dnsEndpoint := &core.Endpoint{Name: "dns-ep-name", Group: "dns-ep-group", URL: "1.1.1.1", DNS: &core.DNS{
|
||||
QueryType: "A",
|
||||
QueryName: "example.com.",
|
||||
}}
|
||||
PublishMetricsForEndpoint(dnsEndpoint, &core.Result{
|
||||
DNSRCode: "NOERROR",
|
||||
Connected: true,
|
||||
Duration: 50 * time.Millisecond,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[DNS_RCODE] == NOERROR", Success: true},
|
||||
},
|
||||
Success: true,
|
||||
})
|
||||
err = testutil.GatherAndCompare(prometheus.Gatherers{prometheus.DefaultGatherer}, bytes.NewBufferString(`
|
||||
# HELP gatus_results_certificate_expiration_seconds Number of seconds until the certificate expires
|
||||
# TYPE gatus_results_certificate_expiration_seconds gauge
|
||||
gatus_results_certificate_expiration_seconds{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 169200
|
||||
# HELP gatus_results_code_total Total number of results by code
|
||||
# TYPE gatus_results_code_total counter
|
||||
gatus_results_code_total{code="200",group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 2
|
||||
gatus_results_code_total{code="NOERROR",group="dns-ep-group",key="dns-ep-group_dns-ep-name",name="dns-ep-name",type="DNS"} 1
|
||||
# HELP gatus_results_connected_total Total number of results in which a connection was successfully established
|
||||
# TYPE gatus_results_connected_total counter
|
||||
gatus_results_connected_total{group="dns-ep-group",key="dns-ep-group_dns-ep-name",name="dns-ep-name",type="DNS"} 1
|
||||
gatus_results_connected_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 2
|
||||
# HELP gatus_results_duration_seconds Duration of the request in seconds
|
||||
# TYPE gatus_results_duration_seconds gauge
|
||||
gatus_results_duration_seconds{group="dns-ep-group",key="dns-ep-group_dns-ep-name",name="dns-ep-name",type="DNS"} 0.05
|
||||
gatus_results_duration_seconds{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",type="HTTP"} 0.125
|
||||
# HELP gatus_results_total Number of results per endpoint
|
||||
# TYPE gatus_results_total counter
|
||||
gatus_results_total{group="dns-ep-group",key="dns-ep-group_dns-ep-name",name="dns-ep-name",success="true",type="DNS"} 1
|
||||
gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",success="false",type="HTTP"} 1
|
||||
gatus_results_total{group="http-ep-group",key="http-ep-group_http-ep-name",name="http-ep-name",success="true",type="HTTP"} 1
|
||||
`), "gatus_results_code_total", "gatus_results_connected_total", "gatus_results_duration_seconds", "gatus_results_total", "gatus_results_certificate_expiration_seconds")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors but got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
package security
|
||||
|
||||
import "log"
|
||||
|
||||
// BasicConfig is the configuration for Basic authentication
|
||||
type BasicConfig struct {
|
||||
// Username is the name which will need to be used for a successful authentication
|
||||
Username string `yaml:"username"`
|
||||
|
||||
// PasswordSha512Hash is the SHA512 hash of the password which will need to be used for a successful authentication
|
||||
// XXX: Remove this on v4.0.0
|
||||
// Deprecated: Use PasswordBcryptHashBase64Encoded instead
|
||||
PasswordSha512Hash string `yaml:"password-sha512"`
|
||||
|
||||
// PasswordBcryptHashBase64Encoded is the base64 encoded string of the Bcrypt hash of the password to use to
|
||||
// authenticate using basic auth.
|
||||
PasswordBcryptHashBase64Encoded string `yaml:"password-bcrypt-base64"`
|
||||
@@ -19,8 +12,5 @@ type BasicConfig struct {
|
||||
|
||||
// isValid returns whether the basic security configuration is valid or not
|
||||
func (c *BasicConfig) isValid() bool {
|
||||
if len(c.PasswordSha512Hash) > 0 {
|
||||
log.Println("WARNING: security.basic.password-sha512 has been deprecated in favor of security.basic.password-bcrypt-base64")
|
||||
}
|
||||
return len(c.Username) > 0 && (len(c.PasswordSha512Hash) == 128 || len(c.PasswordBcryptHashBase64Encoded) > 0)
|
||||
return len(c.Username) > 0 && len(c.PasswordBcryptHashBase64Encoded) > 0
|
||||
}
|
||||
|
||||
@@ -2,26 +2,6 @@ package security
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBasicConfig_IsValidUsingSHA512(t *testing.T) {
|
||||
basicConfig := &BasicConfig{
|
||||
Username: "admin",
|
||||
PasswordSha512Hash: Sha512("test"),
|
||||
}
|
||||
if !basicConfig.isValid() {
|
||||
t.Error("basicConfig should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicConfig_IsValidWhenPasswordIsInvalidUsingSHA512(t *testing.T) {
|
||||
basicConfig := &BasicConfig{
|
||||
Username: "admin",
|
||||
PasswordSha512Hash: "",
|
||||
}
|
||||
if basicConfig.isValid() {
|
||||
t.Error("basicConfig shouldn't have been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicConfig_IsValidUsingBcrypt(t *testing.T) {
|
||||
basicConfig := &BasicConfig{
|
||||
Username: "admin",
|
||||
|
||||
@@ -3,7 +3,6 @@ package security
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/g8"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -82,13 +81,6 @@ func (c *Config) ApplySecurityMiddleware(api *mux.Router) error {
|
||||
_, _ = w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
} else if len(c.Basic.PasswordSha512Hash) > 0 {
|
||||
if !ok || usernameEntered != c.Basic.Username || Sha512(passwordEntered) != strings.ToLower(c.Basic.PasswordSha512Hash) {
|
||||
w.Header().Set("WWW-Authenticate", "Basic")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
@@ -23,10 +23,10 @@ func TestConfig_ApplySecurityMiddleware(t *testing.T) {
|
||||
///////////
|
||||
// BASIC //
|
||||
///////////
|
||||
// SHA512 (DEPRECATED)
|
||||
// Bcrypt
|
||||
c := &Config{Basic: &BasicConfig{
|
||||
Username: "john.doe",
|
||||
PasswordSha512Hash: "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22",
|
||||
Username: "john.doe",
|
||||
PasswordBcryptHashBase64Encoded: "JDJhJDA4JDFoRnpPY1hnaFl1OC9ISlFsa21VS09wOGlPU1ZOTDlHZG1qeTFvb3dIckRBUnlHUmNIRWlT",
|
||||
}}
|
||||
api := mux.NewRouter()
|
||||
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -50,33 +50,6 @@ func TestConfig_ApplySecurityMiddleware(t *testing.T) {
|
||||
if responseRecorder.Code != http.StatusOK {
|
||||
t.Error("expected code to be 200, but was", responseRecorder.Code)
|
||||
}
|
||||
// Bcrypt
|
||||
c = &Config{Basic: &BasicConfig{
|
||||
Username: "john.doe",
|
||||
PasswordBcryptHashBase64Encoded: "JDJhJDA4JDFoRnpPY1hnaFl1OC9ISlFsa21VS09wOGlPU1ZOTDlHZG1qeTFvb3dIckRBUnlHUmNIRWlT",
|
||||
}}
|
||||
api = mux.NewRouter()
|
||||
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
if err := c.ApplySecurityMiddleware(api); err != nil {
|
||||
t.Error("expected no error, but was", err)
|
||||
}
|
||||
// Try to access the route without basic auth
|
||||
request, _ = http.NewRequest("GET", "/test", http.NoBody)
|
||||
responseRecorder = httptest.NewRecorder()
|
||||
api.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusUnauthorized {
|
||||
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||
}
|
||||
// Try again, but with basic auth
|
||||
request, _ = http.NewRequest("GET", "/test", http.NoBody)
|
||||
responseRecorder = httptest.NewRecorder()
|
||||
request.SetBasicAuth("john.doe", "hunter2")
|
||||
api.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusOK {
|
||||
t.Error("expected code to be 200, but was", responseRecorder.Code)
|
||||
}
|
||||
//////////
|
||||
// OIDC //
|
||||
//////////
|
||||
|
||||
@@ -2,4 +2,4 @@ package security
|
||||
|
||||
import "github.com/TwiN/gocache/v2"
|
||||
|
||||
var sessions = gocache.NewCache() // TODO: Move this to storage
|
||||
var sessions = gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed) // TODO: Move this to storage
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sha512 hashes a provided string using SHA512 and returns the resulting hash as a string
|
||||
// Deprecated: Use bcrypt instead
|
||||
func Sha512(s string) string {
|
||||
hash := sha512.New()
|
||||
hash.Write([]byte(s))
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package security
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSha512(t *testing.T) {
|
||||
input := "password"
|
||||
expectedHash := "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86"
|
||||
hash := Sha512(input)
|
||||
if hash != expectedHash {
|
||||
t.Errorf("Expected hash to be '%s', but was '%s'", expectedHash, hash)
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,11 @@ package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSQLStorageRequiresPath = errors.New("sql storage requires a non-empty path to be defined")
|
||||
ErrMemoryStorageDoesNotSupportPath = errors.New("memory storage does not support persistence, use sqlite if you want persistence on file")
|
||||
ErrCannotSetBothFileAndPath = errors.New("file has been deprecated in favor of path: you cannot set both of them")
|
||||
)
|
||||
|
||||
// Config is the configuration for storage
|
||||
@@ -16,16 +14,8 @@ type Config struct {
|
||||
// Path is the path used by the store to achieve persistence
|
||||
// If blank, persistence is disabled.
|
||||
// Note that not all Type support persistence
|
||||
//
|
||||
// XXX: Rename to path for v4.0.0
|
||||
Path string `yaml:"path"`
|
||||
|
||||
// File is the path of the file to use for persistence
|
||||
// If blank, persistence is disabled
|
||||
//
|
||||
// Deprecated
|
||||
File string `yaml:"file"`
|
||||
|
||||
// Type of store
|
||||
// If blank, uses the default in-memory store
|
||||
Type Type `yaml:"type"`
|
||||
@@ -33,14 +23,6 @@ type Config struct {
|
||||
|
||||
// ValidateAndSetDefaults validates the configuration and sets the default values (if applicable)
|
||||
func (c *Config) ValidateAndSetDefaults() error {
|
||||
if len(c.File) > 0 && len(c.Path) > 0 { // XXX: Remove for v4.0.0
|
||||
return ErrCannotSetBothFileAndPath
|
||||
} else if len(c.File) > 0 { // XXX: Remove for v4.0.0
|
||||
log.Println("WARNING: Your configuration is using 'storage.file', which is deprecated in favor of 'storage.path'")
|
||||
log.Println("WARNING: storage.file will be completely removed in v4.0.0, so please update your configuration")
|
||||
log.Println("WARNING: See https://github.com/TwiN/gatus/issues/197")
|
||||
c.Path = c.File
|
||||
}
|
||||
if c.Type == "" {
|
||||
c.Type = TypeMemory
|
||||
}
|
||||
@@ -48,12 +30,7 @@ func (c *Config) ValidateAndSetDefaults() error {
|
||||
return ErrSQLStorageRequiresPath
|
||||
}
|
||||
if c.Type == TypeMemory && len(c.Path) > 0 {
|
||||
log.Println("WARNING: Your configuration is using a storage of type memory with persistence, which has been deprecated")
|
||||
log.Println("WARNING: As of v4.0.0, the default storage type (memory) will not support persistence.")
|
||||
log.Println("WARNING: If you want persistence, use 'storage.type: sqlite' instead of 'storage.type: memory'")
|
||||
log.Println("WARNING: See https://github.com/TwiN/gatus/issues/198")
|
||||
// XXX: Uncomment the following line for v4.0.0
|
||||
//return ErrMemoryStorageDoesNotSupportPath
|
||||
return ErrMemoryStorageDoesNotSupportPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,19 +2,15 @@ package memory
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
||||
"github.com/TwiN/gatus/v3/util"
|
||||
"github.com/TwiN/gocache"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
"github.com/TwiN/gatus/v4/util"
|
||||
"github.com/TwiN/gocache/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -28,11 +24,7 @@ func init() {
|
||||
// Store that leverages gocache
|
||||
type Store struct {
|
||||
sync.RWMutex
|
||||
// Deprecated
|
||||
//
|
||||
// File persistence will no longer be supported as of v4.0.0
|
||||
// XXX: Remove me in v4.0.0
|
||||
file string
|
||||
|
||||
cache *gocache.Cache
|
||||
}
|
||||
|
||||
@@ -42,30 +34,8 @@ type Store struct {
|
||||
// supports eventual persistence.
|
||||
func NewStore(file string) (*Store, error) {
|
||||
store := &Store{
|
||||
file: file,
|
||||
cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize),
|
||||
}
|
||||
// XXX: Remove the block below in v4.0.0 because persistence with the memory store will no longer be supported
|
||||
// XXX: Make sure to also update gocache to v2.0.0
|
||||
if len(file) > 0 {
|
||||
_, err := store.cache.ReadFromFile(file)
|
||||
if err != nil {
|
||||
// XXX: Remove the block below in v4.0.0
|
||||
if data, err2 := os.ReadFile(file); err2 == nil {
|
||||
isFromOldVersion := strings.Contains(string(data), "*core.ServiceStatus")
|
||||
if isFromOldVersion {
|
||||
log.Println("WARNING: Couldn't read file due to recent change in v3.3.0, see https://github.com/TwiN/gatus/issues/191")
|
||||
log.Println("WARNING: Will automatically rename old file to " + file + ".old and overwrite the current file")
|
||||
if err = os.WriteFile(file+".old", data, fs.ModePerm); err != nil {
|
||||
log.Println("WARNING: Tried my best to keep the old file, but it wasn't enough. Sorry, your file will be overwritten :(")
|
||||
}
|
||||
// Return the store regardless of whether there was an error or not
|
||||
return store, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
@@ -221,9 +191,6 @@ func (s *Store) Clear() {
|
||||
|
||||
// Save persists the cache to the store file
|
||||
func (s *Store) Save() error {
|
||||
if len(s.file) > 0 {
|
||||
return s.cache.SaveToFile(s.file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -22,7 +22,7 @@ var (
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
|
||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@ package memory
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
func BenchmarkProcessUptimeAfterResult(b *testing.B) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
)
|
||||
|
||||
func TestProcessUptimeAfterResult(t *testing.T) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
)
|
||||
|
||||
// ShallowCopyEndpointStatus returns a shallow copy of a EndpointStatus with only the results
|
||||
|
||||
@@ -3,9 +3,9 @@ package memory
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
)
|
||||
|
||||
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
)
|
||||
|
||||
func TestAddResult(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user