mirror of
https://github.com/TwiN/gatus.git
synced 2026-09-20 23:04:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5a94979dd | ||
|
|
3c0ea72a5c | ||
|
|
d17e893131 | ||
|
|
7ea34ec8a8 | ||
|
|
f6b99f34db | ||
|
|
37495ac3f3 | ||
|
|
557f696f88 | ||
|
|
c86492dbfd | ||
|
|
8a4db600c9 | ||
|
|
02879e2645 | ||
|
|
00b56ecefd | ||
|
|
47dd18a0b5 | ||
|
|
1a708ebca2 | ||
|
|
5f8e62dad0 | ||
|
|
b74f7758dc | ||
|
|
899c19b2d7 | ||
|
|
35038a63c4 | ||
|
|
7b2af3c514 | ||
|
|
4ab7428599 | ||
|
|
be88af5d48 | ||
|
|
5bb3f6d0a9 | ||
|
|
17c14a7243 | ||
|
|
f44d4055e6 | ||
|
|
38054f57e5 | ||
|
|
33ce0e99b5 | ||
|
|
b5e6466c1d | ||
|
|
f89ecd5c64 | ||
|
|
e434178a5c | ||
|
|
7a3ee1b557 | ||
|
|
e51abaf5bd | ||
|
|
46d6d6c733 | ||
|
|
d9f86f1155 | ||
|
|
01484832fc | ||
|
|
4857b43771 | ||
|
|
52d7cb6f04 | ||
|
|
5c6bf84106 | ||
|
|
c84ae1cd55 | ||
|
|
daf8e3a16f | ||
|
|
df719958cf | ||
|
|
2be81b8e1a | ||
|
|
4bed86dec9 | ||
|
|
072cf20cc6 | ||
|
|
cca421e283 | ||
|
|
a044f1d274 | ||
|
|
9de6334f21 | ||
|
|
f01b66f083 | ||
|
|
262d436533 | ||
|
|
b8ab17eee1 | ||
|
|
7bbd7bcee3 | ||
|
|
4865d12147 | ||
|
|
0713ca1c1a | ||
|
|
dce202d0be | ||
|
|
4673d147db | ||
|
|
0943c45ae6 | ||
|
|
798c4248ff | ||
|
|
1bce4e727e | ||
|
|
1aa94a3365 | ||
|
|
319f460553 | ||
|
|
7daf2b5cac | ||
|
|
f0fc275f67 | ||
|
|
04a682eddc | ||
|
|
2fb807632c | ||
|
|
4b339bca37 | ||
|
|
09c3a6c72b | ||
|
|
755c8bb43a | ||
|
|
9d4a639f31 | ||
|
|
60e6b2b039 | ||
|
|
37f3f964ea | ||
|
|
4a1a8ff380 | ||
|
|
6787fed062 | ||
|
|
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 |
@@ -1,23 +1,41 @@
|
|||||||
## Usage
|
## 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
|
```console
|
||||||
docker-compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
Once you've done the above, you should be able to access the Grafana dashboard at `http://localhost:3000`.
|
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)
|
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)
|
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="true"}[5m])*60) by (key)
|
sum(rate(gatus_results_total{success="false"}[5m])*60) by (key)
|
||||||
```
|
```
|
||||||
+8
-5
@@ -2,15 +2,18 @@ metrics: true
|
|||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health
|
url: https://twin.sh/health
|
||||||
interval: 30s
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
|
- name: example
|
||||||
|
url: https://example.com/
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: github
|
- name: github
|
||||||
url: https://api.github.com/healthz
|
url: https://api.github.com/healthz
|
||||||
interval: 5m
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- name: example
|
|
||||||
url: https://example.com/
|
|
||||||
conditions:
|
|
||||||
- "[STATUS] == 200"
|
|
||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/config/config.yaml
|
- ./config:/config
|
||||||
networks:
|
networks:
|
||||||
- metrics
|
- metrics
|
||||||
|
|
||||||
|
|||||||
+280
-97
@@ -15,8 +15,266 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"gnetId": null,
|
"gnetId": null,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
|
"id": 3,
|
||||||
"links": [],
|
"links": [],
|
||||||
"panels": [
|
"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": {},
|
"aliasColors": {},
|
||||||
"bars": false,
|
"bars": false,
|
||||||
@@ -27,10 +285,10 @@
|
|||||||
"fill": 1,
|
"fill": 1,
|
||||||
"fillGradient": 0,
|
"fillGradient": 0,
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 7,
|
"h": 8,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 12,
|
||||||
"y": 0
|
"y": 8
|
||||||
},
|
},
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"interval": "",
|
"interval": "",
|
||||||
@@ -126,8 +384,8 @@
|
|||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 7,
|
"h": 7,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 0,
|
||||||
"y": 0
|
"y": 16
|
||||||
},
|
},
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"legend": {
|
"legend": {
|
||||||
@@ -204,94 +462,6 @@
|
|||||||
"alignLevel": null
|
"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": {},
|
"aliasColors": {},
|
||||||
"bars": false,
|
"bars": false,
|
||||||
@@ -304,7 +474,7 @@
|
|||||||
"h": 7,
|
"h": 7,
|
||||||
"w": 12,
|
"w": 12,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 7
|
"y": 16
|
||||||
},
|
},
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"legend": {
|
"legend": {
|
||||||
@@ -380,7 +550,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refresh": "10s",
|
"refresh": "1m",
|
||||||
"schemaVersion": 20,
|
"schemaVersion": 20,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -391,9 +561,22 @@
|
|||||||
"from": "now-1h",
|
"from": "now-1h",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {},
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
]
|
||||||
|
},
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Gatus",
|
"title": "Gatus",
|
||||||
"uid": "KPI7Qj1Wk",
|
"uid": "KPI7Qj1Wk",
|
||||||
"version": 1
|
"version": 2
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/config/config.yaml
|
- ./config:/config
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ endpoints:
|
|||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: example-dns-query
|
- 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
|
interval: 5m
|
||||||
dns:
|
dns:
|
||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/config/config.yaml
|
- ./config:/config
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ endpoints:
|
|||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: example-dns-query
|
- 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
|
interval: 5m
|
||||||
dns:
|
dns:
|
||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
@@ -5,5 +5,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/config/config.yaml
|
- ./config:/config
|
||||||
- ./data:/data/
|
- ./data:/data/
|
||||||
@@ -5,4 +5,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/config/config.yaml
|
- ./config:/config
|
||||||
|
|||||||
@@ -54,10 +54,10 @@ spec:
|
|||||||
app: gatus
|
app: gatus
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
|
||||||
app: gatus
|
|
||||||
name: gatus
|
name: gatus
|
||||||
namespace: kube-system
|
namespace: kube-system
|
||||||
|
labels:
|
||||||
|
app: gatus
|
||||||
spec:
|
spec:
|
||||||
serviceAccountName: gatus
|
serviceAccountName: gatus
|
||||||
terminationGracePeriodSeconds: 5
|
terminationGracePeriodSeconds: 5
|
||||||
@@ -76,6 +76,22 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: 50m
|
cpu: 50m
|
||||||
memory: 30M
|
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:
|
volumeMounts:
|
||||||
- mountPath: /config
|
- mountPath: /config
|
||||||
name: gatus-config
|
name: gatus-config
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -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,16 +10,14 @@ on:
|
|||||||
- '*.md'
|
- '*.md'
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Go
|
- uses: actions/setup-go@v3
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: 1.18
|
||||||
- name: Check out code into the Go module directory
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Build binary to make sure it works
|
- name: Build binary to make sure it works
|
||||||
run: go build -mod vendor
|
run: go build -mod vendor
|
||||||
- name: Test
|
- name: Test
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: publish-experimental
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
jobs:
|
||||||
|
publish-experimental:
|
||||||
|
name: publish-experimental
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- 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
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build and push docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64
|
||||||
|
pull: true
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.IMAGE_REPOSITORY }}:experimental
|
||||||
@@ -4,15 +4,17 @@ on:
|
|||||||
workflows: ["build"]
|
workflows: ["build"]
|
||||||
branches: [master]
|
branches: [master]
|
||||||
types: [completed]
|
types: [completed]
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
publish-latest:
|
publish-latest:
|
||||||
name: Publish latest
|
name: publish-latest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
timeout-minutes: 45
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Get image repository
|
- name: Get image repository
|
||||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -20,15 +22,14 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push docker image
|
- name: Build and push docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: ${{ env.IMAGE_REPOSITORY }}:latest
|
||||||
${{ env.IMAGE_REPOSITORY }}:latest
|
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ on:
|
|||||||
types: [published]
|
types: [published]
|
||||||
jobs:
|
jobs:
|
||||||
publish-release:
|
publish-release:
|
||||||
name: Publish release
|
name: publish-release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Get image repository
|
- name: Get image repository
|
||||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
- name: Get the release
|
- name: Get the release
|
||||||
@@ -19,15 +18,14 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push docker image
|
- name: Build and push docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: ${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }},${{ env.IMAGE_REPOSITORY }}:stable
|
||||||
${{ env.IMAGE_REPOSITORY }}:${{ env.RELEASE }}
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus
|
|||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /app/gatus .
|
COPY --from=builder /app/gatus .
|
||||||
COPY --from=builder /app/config.yaml ./config/config.yaml
|
COPY --from=builder /app/config.yaml ./config/config.yaml
|
||||||
COPY --from=builder /app/web/static ./web/static
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2021 TwiN
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
BINARY=gatus
|
BINARY=gatus
|
||||||
|
|
||||||
|
# Because there's a folder called "test", we need to make the target "test" phony
|
||||||
|
.PHONY: test
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -mod vendor -o $(BINARY) .
|
go build -mod vendor -o $(BINARY) .
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@ clean:
|
|||||||
rm $(BINARY)
|
rm $(BINARY)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
sudo go test ./alerting/... ./client/... ./config/... ./controller/... ./core/... ./jsonpath/... ./pattern/... ./security/... ./storage/... ./util/... ./watchdog/... -cover
|
go test ./... -cover
|
||||||
|
|
||||||
|
|
||||||
##########
|
##########
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
||||||
[](https://github.com/TwiN)
|
[](https://github.com/TwiN)
|
||||||
|
|
||||||
Gatus is a health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
||||||
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
||||||
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
||||||
checks can be paired with alerting via Slack, PagerDuty, Discord, Twilio and more.
|
checks can be paired with alerting via Slack, PagerDuty, Discord, Twilio and more.
|
||||||
@@ -26,7 +26,10 @@ For more details, see [Usage](#usage)
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Have any feedback or want to share your good/bad experience with Gatus? Feel free to email me at [feedback@gatus.io](mailto:feedback@gatus.io)
|
Have any feedback or questions? [Create a discussion](https://github.com/TwiN/gatus/discussions/new).
|
||||||
|
|
||||||
|
Like this project? Please consider [sponsoring me](https://github.com/sponsors/TwiN).
|
||||||
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Why Gatus?](#why-gatus)
|
- [Why Gatus?](#why-gatus)
|
||||||
@@ -42,8 +45,10 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
|||||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||||
- [Configuring Email alerts](#configuring-email-alerts)
|
- [Configuring Email alerts](#configuring-email-alerts)
|
||||||
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
||||||
|
- [Configuring Matrix alerts](#configuring-matrix-alerts)
|
||||||
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
||||||
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
||||||
|
- [Configuring Ntfy alerts](#configuring-ntfy-alerts)
|
||||||
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
- [Configuring Opsgenie alerts](#configuring-opsgenie-alerts)
|
||||||
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
||||||
- [Configuring Slack alerts](#configuring-slack-alerts)
|
- [Configuring Slack alerts](#configuring-slack-alerts)
|
||||||
@@ -55,7 +60,9 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
|||||||
- [Maintenance](#maintenance)
|
- [Maintenance](#maintenance)
|
||||||
- [Security](#security)
|
- [Security](#security)
|
||||||
- [Basic](#basic)
|
- [Basic](#basic)
|
||||||
- [OIDC (ALPHA)](#oidc-alpha)
|
- [OIDC](#oidc)
|
||||||
|
- [Metrics](#metrics)
|
||||||
|
- [Remote instances (EXPERIMENTAL)](#remote-instances-experimental)
|
||||||
- [Deployment](#deployment)
|
- [Deployment](#deployment)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
- [Helm Chart](#helm-chart)
|
- [Helm Chart](#helm-chart)
|
||||||
@@ -71,17 +78,20 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
|||||||
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
|
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
|
||||||
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
|
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
|
||||||
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
|
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
|
||||||
|
- [Monitoring domain expiration](#monitoring-domain-expiration)
|
||||||
- [disable-monitoring-lock](#disable-monitoring-lock)
|
- [disable-monitoring-lock](#disable-monitoring-lock)
|
||||||
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
||||||
- [Endpoint groups](#endpoint-groups)
|
- [Endpoint groups](#endpoint-groups)
|
||||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||||
- [Badges](#badges)
|
- [Badges](#badges)
|
||||||
- [Uptime](#uptime)
|
- [Uptime](#uptime)
|
||||||
|
- [Health](#health)
|
||||||
- [Response time](#response-time)
|
- [Response time](#response-time)
|
||||||
- [API](#api)
|
- [API](#api)
|
||||||
- [High level design overview](#high-level-design-overview)
|
- [High level design overview](#high-level-design-overview)
|
||||||
- [Sponsors](#sponsors)
|
- [Sponsors](#sponsors)
|
||||||
|
|
||||||
|
|
||||||
## Why Gatus?
|
## Why Gatus?
|
||||||
Before getting into the specifics, I want to address the most common question:
|
Before getting into the specifics, I want to address the most common question:
|
||||||
> Why would I use Gatus when I can just use Prometheus’ Alertmanager, Cloudwatch or even Splunk?
|
> Why would I use Gatus when I can just use Prometheus’ Alertmanager, Cloudwatch or even Splunk?
|
||||||
@@ -113,6 +123,7 @@ The main features of Gatus are:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
By default, the configuration file is expected to be at `config/config.yaml`.
|
By default, the configuration file is expected to be at `config/config.yaml`.
|
||||||
|
|
||||||
@@ -145,48 +156,53 @@ If you want to test it locally, see [Docker](#docker).
|
|||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
|:------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|
|
||||||
| `debug` | Whether to enable debug logs. | `false` |
|
| `debug` | Whether to enable debug logs. | `false` |
|
||||||
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
| `metrics` | Whether to expose metrics at /metrics. | `false` |
|
||||||
| `storage` | [Storage configuration](#storage) | `{}` |
|
| `storage` | [Storage configuration](#storage) | `{}` |
|
||||||
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
| `endpoints` | List of endpoints to monitor. | Required `[]` |
|
||||||
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
| `endpoints[].enabled` | Whether to monitor the endpoint. | `true` |
|
||||||
| `endpoints[].name` | Name of the endpoint. Can be anything. | Required `""` |
|
| `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[].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[].url` | URL to send the request to. | Required `""` |
|
||||||
| `endpoints[].method` | Request method. | `GET` |
|
| `endpoints[].method` | Request method. | `GET` |
|
||||||
| `endpoints[].conditions` | Conditions used to determine the health of the endpoint. <br />See [Conditions](#conditions). | `[]` |
|
| `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[].interval` | Duration to wait between every status check. | `60s` |
|
||||||
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
| `endpoints[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`). | `false` |
|
||||||
| `endpoints[].body` | Request body. | `""` |
|
| `endpoints[].body` | Request body. | `""` |
|
||||||
| `endpoints[].headers` | Request headers. | `{}` |
|
| `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` | 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-type` | Query type (e.g. MX) | `""` |
|
||||||
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
| `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[].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[].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[].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[].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[].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[].alerts[].description` | Description of the alert. Will be included in the alert sent. | `""` |
|
||||||
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
| `endpoints[].client` | [Client configuration](#client-configuration). | `{}` |
|
||||||
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
| `endpoints[].ui` | UI configuration at the endpoint level. | `{}` |
|
||||||
| `endpoints[].ui.hide-hostname` | Whether to include the hostname in the result. | `false` |
|
| `endpoints[].ui.hide-hostname` | Whether to hide the hostname in the result. | `false` |
|
||||||
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
| `endpoints[].ui.hide-url` | Whether to ensure the URL is not displayed in the results. Useful if the URL contains a token. | `false` |
|
||||||
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||||
| `security` | [Security configuration](#security). | `{}` |
|
| `endpoints[].ui.badge.reponse-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
||||||
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
| `alerting` | [Alerting configuration](#alerting). | `{}` |
|
||||||
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
|
| `security` | [Security configuration](#security). | `{}` |
|
||||||
| `web` | Web configuration. | `{}` |
|
| `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock). | `false` |
|
||||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
| `skip-invalid-config-update` | Whether to ignore invalid configuration update. <br />See [Reloading configuration on the fly](#reloading-configuration-on-the-fly). | `false` |
|
||||||
| `web.port` | Port to listen on. | `8080` |
|
| `web` | Web configuration. | `{}` |
|
||||||
| `ui` | UI configuration. | `{}` |
|
| `web.address` | Address to listen on. | `0.0.0.0` |
|
||||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
| `web.port` | Port to listen on. | `8080` |
|
||||||
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
|
| `ui` | UI configuration. | `{}` |
|
||||||
| `ui.logo` | URL to the logo to display. | `""` |
|
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
||||||
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
| `ui.header` | Header at the top of the dashboard. | `Health Status` |
|
||||||
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
|
| `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
|
### Conditions
|
||||||
@@ -213,18 +229,20 @@ Here are some examples of conditions you can use:
|
|||||||
| `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` |
|
| `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` |
|
||||||
| `[BODY].id == any(1, 2)` | Value at JSONPath `$.id` is equal to `1` or `2` | 1, 2 | 3, 4, 5 |
|
| `[BODY].id == any(1, 2)` | Value at JSONPath `$.id` is equal to `1` or `2` | 1, 2 | 3, 4, 5 |
|
||||||
| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | 49h, 50h, 123h | 1h, 24h, ... |
|
| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | 49h, 50h, 123h | 1h, 24h, ... |
|
||||||
|
| `[DOMAIN_EXPIRATION] > 720h` | The domain must expire in more than 720h | 4000h | 1h, 24h, ... |
|
||||||
|
|
||||||
|
|
||||||
#### Placeholders
|
#### Placeholders
|
||||||
| Placeholder | Description | Example of resolved value |
|
| Placeholder | Description | Example of resolved value |
|
||||||
|:---------------------------|:---------------------------------------------------------|:---------------------------------------------|
|
|:---------------------------|:------------------------------------------------------------------------------------------|:---------------------------------------------|
|
||||||
| `[STATUS]` | Resolves into the HTTP status of the request | 404 |
|
| `[STATUS]` | Resolves into the HTTP status of the request | `404` |
|
||||||
| `[RESPONSE_TIME]` | Resolves into the response time the request took, in ms | 10 |
|
| `[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 |
|
| `[IP]` | Resolves into the IP of the target host | `192.168.0.232` |
|
||||||
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}` |
|
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}` |
|
||||||
| `[CONNECTED]` | Resolves into whether a connection could be established | `true` |
|
| `[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) |
|
| `[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 |
|
| `[DOMAIN_EXPIRATION]` | Resolves into the duration before the domain expires (valid units are "s", "m", "h".) | `24h`, `48h`, `1234h56m78s` |
|
||||||
|
| `[DNS_RCODE]` | Resolves into the DNS status of the response | `NOERROR` |
|
||||||
|
|
||||||
|
|
||||||
#### Functions
|
#### Functions
|
||||||
@@ -239,11 +257,12 @@ Here are some examples of conditions you can use:
|
|||||||
|
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:---------------|:-------------------------------------------------------------------------------|:-----------|
|
|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|
|
||||||
| `storage` | Storage configuration | `{}` |
|
| `storage` | Storage configuration | `{}` |
|
||||||
| `storage.path` | Path to persist the data in. Only supported for types `sqlite` and `postgres`. | `""` |
|
| `storage.path` | Path to persist the data in. Only supported for types `sqlite` and `postgres`. | `""` |
|
||||||
| `storage.type` | Type of storage. Valid types: `memory`, `sqlite`, `postgres`. | `"memory"` |
|
| `storage.type` | Type of storage. Valid types: `memory`, `sqlite`, `postgres`. | `"memory"` |
|
||||||
|
| `storage.caching` | Whether to use write-through caching. Improves loading time for large dashboards. <br />Only supported if `storage.type` is `sqlite` or `postgres` | `false` |
|
||||||
|
|
||||||
- If `storage.type` is `memory` (default):
|
- If `storage.type` is `memory` (default):
|
||||||
```yaml
|
```yaml
|
||||||
@@ -273,11 +292,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
|
In order to support a wide range of environments, each monitored endpoint has a unique configuration for
|
||||||
the client used to send the request.
|
the client used to send the request.
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-------------------------|:------------------------------------------------------------------------|:--------|
|
|:------------------------------|:---------------------------------------------------------------------------|:----------------|
|
||||||
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
||||||
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
||||||
| `client.timeout` | Duration before timing out. | `10s` |
|
| `client.timeout` | Duration before timing out. | `10s` |
|
||||||
|
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
|
||||||
|
| `client.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
|
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.
|
in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
|
||||||
@@ -304,6 +329,32 @@ endpoints:
|
|||||||
- "[STATUS] == 200"
|
- "[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
|
### Alerting
|
||||||
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
||||||
@@ -317,8 +368,10 @@ ignored.
|
|||||||
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
|
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
|
||||||
| `alerting.email` | Configuration for alerts of type `email`. <br />See [Configuring Email alerts](#configuring-email-alerts). | `{}` |
|
| `alerting.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.googlechat` | Configuration for alerts of type `googlechat`. <br />See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` |
|
||||||
|
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
|
||||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
|
| `alerting.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.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
|
||||||
|
| `alerting.ntfy` | Configuration for alerts of type `ntfy`. <br />See [Configuring Ntfy alerts](#configuring-ntfy-alerts). | `{}` |
|
||||||
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
| `alerting.opsgenie` | Configuration for alerts of type `opsgenie`. <br />See [Configuring Opsgenie alerts](#configuring-opsgenie-alerts). | `{}` |
|
||||||
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
|
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty`. <br />See [Configuring PagerDuty alerts](#configuring-pagerduty-alerts). | `{}` |
|
||||||
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
||||||
@@ -329,11 +382,15 @@ ignored.
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring Discord alerts
|
#### Configuring Discord alerts
|
||||||
| Parameter | Description | Default |
|
|
||||||
|:---------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
| Parameter | Description | Default |
|
||||||
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
|
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
||||||
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
| `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
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -343,7 +400,7 @@ alerting:
|
|||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: "https://twin.sh/health"
|
url: "https://twin.sh/health"
|
||||||
interval: 30s
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- "[BODY].status == UP"
|
- "[BODY].status == UP"
|
||||||
@@ -357,16 +414,20 @@ endpoints:
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring Email alerts
|
#### Configuring Email alerts
|
||||||
| Parameter | Description | Default |
|
|
||||||
|:-------------------------------|:-------------------------------------------------------------------------------------------|:----------------------|
|
| Parameter | Description | Default |
|
||||||
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
||||||
| `alerting.email.username` | Username of the SMTP server used to send the alert. If empty, uses `alerting.email.from`. | `""` |
|
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
||||||
| `alerting.email.password` | Password of the SMTP server 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.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
| `alerting.email.password` | Password of the SMTP server used to send the alert | Required `""` |
|
||||||
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
||||||
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
|
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
||||||
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
| `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
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -377,6 +438,11 @@ alerting:
|
|||||||
host: "mail.example.com"
|
host: "mail.example.com"
|
||||||
port: 587
|
port: 587
|
||||||
to: "recipient1@example.com,recipient2@example.com"
|
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:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@@ -391,27 +457,45 @@ endpoints:
|
|||||||
enabled: true
|
enabled: true
|
||||||
description: "healthcheck failed"
|
description: "healthcheck failed"
|
||||||
send-on-resolved: true
|
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.
|
**NOTE:** Some mail servers are painfully slow.
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Google Chat alerts
|
#### Configuring Google Chat alerts
|
||||||
| Parameter | Description | Default |
|
|
||||||
|:------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
| Parameter | Description | Default |
|
||||||
| `alerting.googlechat` | Configuration for alerts of type `googlechat` | `{}` |
|
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.googlechat.webhook-url` | Google Chat Webhook URL | Required `""` |
|
| `alerting.googlechat` | Configuration for alerts of type `googlechat` | `{}` |
|
||||||
| `alerting.googlechat.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
| `alerting.googlechat.webhook-url` | Google Chat Webhook URL | Required `""` |
|
||||||
| `alerting.googlechat.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
| `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
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
mattermost:
|
googlechat:
|
||||||
webhook-url: "https://chat.googleapis.com/v1/spaces/*******/messages?key=**********&token=********"
|
webhook-url: "https://chat.googleapis.com/v1/spaces/*******/messages?key=**********&token=********"
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: "https://twin.sh/health"
|
url: "https://twin.sh/health"
|
||||||
interval: 30s
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- "[BODY].status == UP"
|
- "[BODY].status == UP"
|
||||||
@@ -423,17 +507,49 @@ endpoints:
|
|||||||
send-on-resolved: true
|
send-on-resolved: true
|
||||||
```
|
```
|
||||||
|
|
||||||
Here's an example of what the notifications look like:
|
|
||||||
|
|
||||||

|
#### Configuring Matrix alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------|
|
||||||
|
| `alerting.matrix` | Configuration for alerts of type `matrix` | `{}` |
|
||||||
|
| `alerting.matrix.server-url` | Homeserver URL | `https://matrix-client.matrix.org` |
|
||||||
|
| `alerting.matrix.access-token` | Bot user access token (see https://webapps.stackexchange.com/q/131056) | Required `""` |
|
||||||
|
| `alerting.matrix.internal-room-id` | Internal room ID of room to send alerts to (can be found in Room Settings > Advanced) | Required `""` |
|
||||||
|
| `alerting.matrix.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
matrix:
|
||||||
|
server-url: "https://matrix-client.matrix.org"
|
||||||
|
access-token: "123456"
|
||||||
|
internal-room-id: "!example:matrix.org"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
interval: 5m
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
|
alerts:
|
||||||
|
- type: matrix
|
||||||
|
enabled: true
|
||||||
|
send-on-resolved: true
|
||||||
|
description: "healthcheck failed"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Mattermost alerts
|
#### Configuring Mattermost alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
|
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
|
||||||
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
|
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
|
||||||
| `alerting.mattermost.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
| `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.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
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -445,7 +561,7 @@ alerting:
|
|||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: "https://twin.sh/health"
|
url: "https://twin.sh/health"
|
||||||
interval: 30s
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- "[BODY].status == UP"
|
- "[BODY].status == UP"
|
||||||
@@ -461,10 +577,11 @@ Here's an example of what the notifications look like:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
#### Configuring Messagebird alerts
|
#### Configuring Messagebird alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` |
|
| `alerting.messagebird` | Configuration for alerts of type `messagebird` | `{}` |
|
||||||
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
|
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
|
||||||
| `alerting.messagebird.originator` | The sender of the message | Required `""` |
|
| `alerting.messagebird.originator` | The sender of the message | Required `""` |
|
||||||
| `alerting.messagebird.recipients` | The recipients of the message | Required `""` |
|
| `alerting.messagebird.recipients` | The recipients of the message | Required `""` |
|
||||||
@@ -480,7 +597,7 @@ alerting:
|
|||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
interval: 30s
|
interval: 5m
|
||||||
url: "https://twin.sh/health"
|
url: "https://twin.sh/health"
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
@@ -495,6 +612,42 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Configuring Ntfy alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:------------------------------|:-------------------------------------------------------------------------------------------|:------------------|
|
||||||
|
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
|
||||||
|
| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
|
||||||
|
| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
|
||||||
|
| `alerting.ntfy.priority` | The priority of the alert | `3` |
|
||||||
|
| `alerting.ntfy.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
|
||||||
|
[ntfy](https://github.com/binwiederhier/ntfy) is an amazing project that allows you to subscribe to desktop
|
||||||
|
and mobile notifications, making it an awesome addition to Gatus.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
ntfy:
|
||||||
|
topic: "gatus-test-topic"
|
||||||
|
priority: 2
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
failure-threshold: 3
|
||||||
|
send-on-resolved: true
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
interval: 5m
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
|
alerts:
|
||||||
|
- type: ntfy
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Opsgenie alerts
|
#### Configuring Opsgenie alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:----------------------------------|:-------------------------------------------------------------------------------------------|:---------------------|
|
|:----------------------------------|:-------------------------------------------------------------------------------------------|:---------------------|
|
||||||
@@ -542,8 +695,8 @@ alerting:
|
|||||||
# You can also add group-specific integration keys, which will
|
# You can also add group-specific integration keys, which will
|
||||||
# override the integration key above for the specified groups
|
# override the integration key above for the specified groups
|
||||||
overrides:
|
overrides:
|
||||||
- group: "core"
|
- group: "core"
|
||||||
integration-key: "********************************"
|
integration-key: "********************************"
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@@ -579,12 +732,14 @@ endpoints:
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring Slack alerts
|
#### Configuring Slack alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
|
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
|
||||||
| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` |
|
| `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.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
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
slack:
|
slack:
|
||||||
@@ -616,16 +771,25 @@ Here's an example of what the notifications look like:
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring Teams alerts
|
#### Configuring Teams alerts
|
||||||
| Parameter | Description | Default |
|
|
||||||
|:-------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
| Parameter | Description | Default |
|
||||||
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
|
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
||||||
| `alerting.teams.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
| `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
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
teams:
|
teams:
|
||||||
webhook-url: "https://********.webhook.office.com/webhookb2/************"
|
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:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@@ -640,19 +804,35 @@ endpoints:
|
|||||||
enabled: true
|
enabled: true
|
||||||
description: "healthcheck failed"
|
description: "healthcheck failed"
|
||||||
send-on-resolved: true
|
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:
|
Here's an example of what the notifications look like:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
#### Configuring Telegram alerts
|
#### Configuring Telegram alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:----------------------------------|:-------------------------------------------------------------------------------------------|:---------------------------|
|
||||||
| `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` |
|
| `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` |
|
||||||
| `alerting.telegram.token` | Telegram Bot Token | Required `""` |
|
| `alerting.telegram.token` | Telegram Bot Token | Required `""` |
|
||||||
| `alerting.telegram.id` | Telegram User ID | 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 |
|
| `alerting.telegram.api-url` | Telegram API URL | `https://api.telegram.org` |
|
||||||
|
| `alerting.telegram.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
||||||
|
| `alerting.telegram.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@@ -731,8 +911,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,
|
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.
|
then automatically roll it back.
|
||||||
|
|
||||||
The placeholders `[ALERT_DESCRIPTION]` and `[ENDPOINT_NAME]` are automatically substituted for the alert description and
|
Furthermore, you may use the following placeholders in the body (`alerting.custom.body`) and in the url (`alerting.custom.url`):
|
||||||
the endpoint name. These placeholders can be used 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
|
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.
|
`[ALERT_TRIGGERED_OR_RESOLVED]` placeholder to differentiate the notifications.
|
||||||
@@ -747,7 +930,7 @@ alerting:
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
body: |
|
body: |
|
||||||
{
|
{
|
||||||
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_GROUP] - [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
||||||
}
|
}
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@@ -870,6 +1053,7 @@ endpoints:
|
|||||||
- type: pagerduty
|
- type: pagerduty
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
If you have maintenance windows, you may not want to be annoyed by alerts.
|
If you have maintenance windows, you may not want to be annoyed by alerts.
|
||||||
To do that, you'll have to use the maintenance configuration:
|
To do that, you'll have to use the maintenance configuration:
|
||||||
@@ -908,6 +1092,7 @@ maintenance:
|
|||||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||||
|
|
||||||
|
|
||||||
#### Basic
|
#### Basic
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:----------------------------------------|:-----------------------------------------------------------------------------------|:--------------|
|
|:----------------------------------------|:-----------------------------------------------------------------------------------|:--------------|
|
||||||
@@ -926,7 +1111,8 @@ security:
|
|||||||
**WARNING:** Make sure to carefully select to cost of the bcrypt hash. The higher the cost, the longer it takes to compute the hash,
|
**WARNING:** Make sure to carefully select to cost of the bcrypt hash. The higher the cost, the longer it takes to compute the hash,
|
||||||
and basic auth verifies the password against the hash on every request. As of 2022-01-08, I suggest a cost of 8.
|
and basic auth verifies the password against the hash on every request. As of 2022-01-08, I suggest a cost of 8.
|
||||||
|
|
||||||
#### OIDC (ALPHA)
|
|
||||||
|
#### OIDC
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:---------------------------------|:---------------------------------------------------------------|:--------------|
|
|:---------------------------------|:---------------------------------------------------------------|:--------------|
|
||||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||||
@@ -949,7 +1135,48 @@ security:
|
|||||||
#allowed-subjects: ["johndoe@example.com"]
|
#allowed-subjects: ["johndoe@example.com"]
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE:** The OIDC feature is currently in Alpha. Breaking changes may occur. Use this feature at your own risk.
|
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
|
||||||
|
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
|
||||||
|
### Remote instances (EXPERIMENTAL)
|
||||||
|
This feature allows you to retrieve endpoint statuses from a remote Gatus instance.
|
||||||
|
|
||||||
|
There are two main use cases for this:
|
||||||
|
- You have multiple Gatus instances running on different machines, and you wish to visually expose the statuses through a single dashboard
|
||||||
|
- You have one or more Gatus instances that are not publicly accessible (e.g. behind a firewall), and you wish to retrieve
|
||||||
|
|
||||||
|
This is an experimental feature. It may be removed or updated in a breaking manner at any time. Furthermore,
|
||||||
|
there are known issues with this feature. If you'd like to provide some feedback, please write a comment in [#64](https://github.com/TwiN/gatus/issues/64).
|
||||||
|
Use at your own risk.
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:-----------------------------------|:---------------------------------------------|:---------------|
|
||||||
|
| `remote` | Remote configuration | `{}` |
|
||||||
|
| `remote.instances` | List of remote instances | Required `[]` |
|
||||||
|
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
||||||
|
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
remote:
|
||||||
|
instances:
|
||||||
|
- endpoint-prefix: "status.example.org-"
|
||||||
|
url: "https://status.example.org/api/v1/endpoints/statuses"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
@@ -998,7 +1225,6 @@ and [helmfile example](https://github.com/avakarev/gatus-chart#helmfileyaml-exam
|
|||||||
Gatus can be deployed on Terraform by using the following module: [terraform-kubernetes-gatus](https://github.com/TwiN/terraform-kubernetes-gatus).
|
Gatus can be deployed on Terraform by using the following module: [terraform-kubernetes-gatus](https://github.com/TwiN/terraform-kubernetes-gatus).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Running the tests
|
## Running the tests
|
||||||
```console
|
```console
|
||||||
go test ./... -mod vendor
|
go test ./... -mod vendor
|
||||||
@@ -1071,11 +1297,11 @@ simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
|
|||||||
|
|
||||||
|
|
||||||
### Default timeouts
|
### Default timeouts
|
||||||
| Endpoint type | Timeout |
|
| Endpoint type | Timeout |
|
||||||
|:------------- |:------- |
|
|:---------------|:--------|
|
||||||
| HTTP | 10s
|
| HTTP | 10s |
|
||||||
| TCP | 10s
|
| TCP | 10s |
|
||||||
| ICMP | 10s
|
| ICMP | 10s |
|
||||||
|
|
||||||
To modify the timeout, see [Client configuration](#client-configuration).
|
To modify the timeout, see [Client configuration](#client-configuration).
|
||||||
|
|
||||||
@@ -1095,6 +1321,8 @@ endpoints:
|
|||||||
Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `endpoints[].headers`,
|
Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `endpoints[].headers`,
|
||||||
`endpoints[].method` and `endpoints[].graphql` are not supported for TCP endpoints.
|
`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
|
**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
|
something at the given address listening to the given port, and that a connection to that address was successfully
|
||||||
established.
|
established.
|
||||||
@@ -1168,6 +1396,25 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Monitoring domain expiration
|
||||||
|
You can monitor the expiration of a domain with all endpoint types except for DNS by using the `[DOMAIN_EXPIRATION]`
|
||||||
|
placeholder:
|
||||||
|
```yaml
|
||||||
|
endpoints:
|
||||||
|
- name: check-domain-and-certificate-expiration
|
||||||
|
url: "https://example.org"
|
||||||
|
interval: 1h
|
||||||
|
conditions:
|
||||||
|
- "[DOMAIN_EXPIRATION] > 720h"
|
||||||
|
- "[CERTIFICATE_EXPIRATION] > 240h"
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE**: The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois)
|
||||||
|
and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`).
|
||||||
|
To prevent the WHOIS service from throttling your IP address if you send too many requests, Gatus will prevent you from
|
||||||
|
using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`.
|
||||||
|
|
||||||
|
|
||||||
### disable-monitoring-lock
|
### disable-monitoring-lock
|
||||||
Setting `disable-monitoring-lock` to `true` means that multiple endpoints could be monitored at the same time.
|
Setting `disable-monitoring-lock` to `true` means that multiple endpoints could be monitored at the same time.
|
||||||
|
|
||||||
@@ -1239,7 +1486,7 @@ endpoints:
|
|||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: random endpoint that isn't part of a group
|
- name: random endpoint that is not part of a group
|
||||||
url: "https://example.org/"
|
url: "https://example.org/"
|
||||||
interval: 5m
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
@@ -1265,8 +1512,9 @@ web:
|
|||||||
port: ${PORT}
|
port: ${PORT}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Badges
|
### Badges
|
||||||
### Uptime
|
#### Uptime
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -1299,7 +1547,24 @@ Example:
|
|||||||
If you'd like to see a visual example of each badge 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
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -1313,6 +1578,26 @@ Where:
|
|||||||
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
||||||
|
|
||||||
|
|
||||||
|
##### How to change the color thresholds of the response time badge
|
||||||
|
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
|
||||||
|
The values in the array correspond to the levels [Awesome, Great, Good, Passable, Bad]
|
||||||
|
All five values must be given in milliseconds (ms).
|
||||||
|
|
||||||
|
```
|
||||||
|
endpoints:
|
||||||
|
- name: nas
|
||||||
|
group: internal
|
||||||
|
url: "https://example.org/"
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
ui:
|
||||||
|
badge:
|
||||||
|
response-time:
|
||||||
|
thresholds: [550, 850, 1350, 1650, 1750]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### API
|
### API
|
||||||
Gatus provides a simple read-only API that 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.
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,21 @@ const (
|
|||||||
// TypeGoogleChat is the Type for the googlechat alerting provider
|
// TypeGoogleChat is the Type for the googlechat alerting provider
|
||||||
TypeGoogleChat Type = "googlechat"
|
TypeGoogleChat Type = "googlechat"
|
||||||
|
|
||||||
|
// TypeMatrix is the Type for the matrix alerting provider
|
||||||
|
TypeMatrix Type = "matrix"
|
||||||
|
|
||||||
// TypeMattermost is the Type for the mattermost alerting provider
|
// TypeMattermost is the Type for the mattermost alerting provider
|
||||||
TypeMattermost Type = "mattermost"
|
TypeMattermost Type = "mattermost"
|
||||||
|
|
||||||
// TypeMessagebird is the Type for the messagebird alerting provider
|
// TypeMessagebird is the Type for the messagebird alerting provider
|
||||||
TypeMessagebird Type = "messagebird"
|
TypeMessagebird Type = "messagebird"
|
||||||
|
|
||||||
|
// TypeNtfy is the Type for the ntfy alerting provider
|
||||||
|
TypeNtfy Type = "ntfy"
|
||||||
|
|
||||||
|
// TypeOpsgenie is the Type for the opsgenie alerting provider
|
||||||
|
TypeOpsgenie Type = "opsgenie"
|
||||||
|
|
||||||
// TypePagerDuty is the Type for the pagerduty alerting provider
|
// TypePagerDuty is the Type for the pagerduty alerting provider
|
||||||
TypePagerDuty Type = "pagerduty"
|
TypePagerDuty Type = "pagerduty"
|
||||||
|
|
||||||
@@ -37,7 +46,4 @@ const (
|
|||||||
|
|
||||||
// TypeTwilio is the Type for the twilio alerting provider
|
// TypeTwilio is the Type for the twilio alerting provider
|
||||||
TypeTwilio Type = "twilio"
|
TypeTwilio Type = "twilio"
|
||||||
|
|
||||||
// TypeOpsgenie is the Type for the opsgenie alerting provider
|
|
||||||
TypeOpsgenie Type = "opsgenie"
|
|
||||||
)
|
)
|
||||||
|
|||||||
+43
-23
@@ -1,20 +1,22 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider"
|
"github.com/TwiN/gatus/v4/alerting/provider"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
"github.com/TwiN/gatus/v4/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
"github.com/TwiN/gatus/v4/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/email"
|
"github.com/TwiN/gatus/v4/alerting/provider/email"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v4/alerting/provider/googlechat"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v4/alerting/provider/matrix"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v4/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/opsgenie"
|
"github.com/TwiN/gatus/v4/alerting/provider/messagebird"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v4/alerting/provider/ntfy"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/slack"
|
"github.com/TwiN/gatus/v4/alerting/provider/opsgenie"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/teams"
|
"github.com/TwiN/gatus/v4/alerting/provider/pagerduty"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v4/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/twilio"
|
"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
|
// Config is the configuration for alerting providers
|
||||||
@@ -31,12 +33,21 @@ type Config struct {
|
|||||||
// Email is the configuration for the email alerting provider
|
// Email is the configuration for the email alerting provider
|
||||||
Email *email.AlertProvider `yaml:"email,omitempty"`
|
Email *email.AlertProvider `yaml:"email,omitempty"`
|
||||||
|
|
||||||
|
// Matrix is the configuration for the matrix alerting provider
|
||||||
|
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
|
||||||
|
|
||||||
// Mattermost is the configuration for the mattermost alerting provider
|
// Mattermost is the configuration for the mattermost alerting provider
|
||||||
Mattermost *mattermost.AlertProvider `yaml:"mattermost,omitempty"`
|
Mattermost *mattermost.AlertProvider `yaml:"mattermost,omitempty"`
|
||||||
|
|
||||||
// Messagebird is the configuration for the messagebird alerting provider
|
// Messagebird is the configuration for the messagebird alerting provider
|
||||||
Messagebird *messagebird.AlertProvider `yaml:"messagebird,omitempty"`
|
Messagebird *messagebird.AlertProvider `yaml:"messagebird,omitempty"`
|
||||||
|
|
||||||
|
// Ntfy is the configuration for the ntfy alerting provider
|
||||||
|
Ntfy *ntfy.AlertProvider `yaml:"ntfy,omitempty"`
|
||||||
|
|
||||||
|
// Opsgenie is the configuration for the opsgenie alerting provider
|
||||||
|
Opsgenie *opsgenie.AlertProvider `yaml:"opsgenie,omitempty"`
|
||||||
|
|
||||||
// PagerDuty is the configuration for the pagerduty alerting provider
|
// PagerDuty is the configuration for the pagerduty alerting provider
|
||||||
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"`
|
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"`
|
||||||
|
|
||||||
@@ -51,9 +62,6 @@ type Config struct {
|
|||||||
|
|
||||||
// Twilio is the configuration for the twilio alerting provider
|
// Twilio is the configuration for the twilio alerting provider
|
||||||
Twilio *twilio.AlertProvider `yaml:"twilio,omitempty"`
|
Twilio *twilio.AlertProvider `yaml:"twilio,omitempty"`
|
||||||
|
|
||||||
// Opsgenie is the configuration for the opsgenie alerting provider
|
|
||||||
Opsgenie *opsgenie.AlertProvider `yaml:"opsgenie,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
|
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
|
||||||
@@ -65,12 +73,6 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return config.Custom
|
return config.Custom
|
||||||
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.TypeDiscord:
|
case alert.TypeDiscord:
|
||||||
if config.Discord == nil {
|
if config.Discord == nil {
|
||||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||||
@@ -83,6 +85,18 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return config.Email
|
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.TypeMatrix:
|
||||||
|
if config.Matrix == nil {
|
||||||
|
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return config.Matrix
|
||||||
case alert.TypeMattermost:
|
case alert.TypeMattermost:
|
||||||
if config.Mattermost == nil {
|
if config.Mattermost == nil {
|
||||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||||
@@ -95,6 +109,12 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return config.Messagebird
|
return config.Messagebird
|
||||||
|
case alert.TypeNtfy:
|
||||||
|
if config.Ntfy == nil {
|
||||||
|
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return config.Ntfy
|
||||||
case alert.TypeOpsgenie:
|
case alert.TypeOpsgenie:
|
||||||
if config.Opsgenie == nil {
|
if config.Opsgenie == nil {
|
||||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is 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"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
||||||
@@ -50,48 +50,28 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription string, resolved bool) *http.Request {
|
func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
||||||
body := provider.Body
|
body, url, method := provider.Body, provider.URL, provider.Method
|
||||||
providerURL := provider.URL
|
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
method := provider.Method
|
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
|
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
|
||||||
if strings.Contains(body, "[ALERT_DESCRIPTION]") {
|
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
|
||||||
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alertDescription)
|
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
|
||||||
}
|
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
|
||||||
if strings.Contains(body, "[SERVICE_NAME]") { // XXX: Remove this in v4.0.0
|
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
|
||||||
body = strings.ReplaceAll(body, "[SERVICE_NAME]", endpointName)
|
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
|
||||||
}
|
if resolved {
|
||||||
if strings.Contains(body, "[ENDPOINT_NAME]") {
|
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpointName)
|
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
}
|
} else {
|
||||||
if strings.Contains(body, "[ALERT_TRIGGERED_OR_RESOLVED]") {
|
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
|
||||||
if resolved {
|
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(method) == 0 {
|
if len(method) == 0 {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
}
|
}
|
||||||
bodyBuffer := bytes.NewBuffer([]byte(body))
|
bodyBuffer := bytes.NewBuffer([]byte(body))
|
||||||
request, _ := http.NewRequest(method, providerURL, bodyBuffer)
|
request, _ := http.NewRequest(method, url, bodyBuffer)
|
||||||
for k, v := range provider.Headers {
|
for k, v := range provider.Headers {
|
||||||
request.Header.Set(k, v)
|
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 {
|
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)
|
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
package custom
|
package custom
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{URL: ""}
|
t.Run("invalid-provider", func(t *testing.T) {
|
||||||
if invalidProvider.IsValid() {
|
invalidProvider := AlertProvider{URL: ""}
|
||||||
t.Error("provider shouldn't have been valid")
|
if invalidProvider.IsValid() {
|
||||||
}
|
t.Error("provider shouldn't have been valid")
|
||||||
validProvider := AlertProvider{URL: "https://example.com"}
|
}
|
||||||
if !validProvider.IsValid() {
|
})
|
||||||
t.Error("provider should've been valid")
|
t.Run("valid-provider", func(t *testing.T) {
|
||||||
}
|
validProvider := AlertProvider{URL: "https://example.com"}
|
||||||
|
if validProvider.ClientConfig != nil {
|
||||||
|
t.Error("provider client config should have been nil prior to IsValid() being executed")
|
||||||
|
}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
if validProvider.ClientConfig == nil {
|
||||||
|
t.Error("provider client config should have been set after IsValid() was executed")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_Send(t *testing.T) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
@@ -99,77 +110,103 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_buildHTTPRequestWhenResolved(t *testing.T) {
|
func TestAlertProvider_buildHTTPRequest(t *testing.T) {
|
||||||
const (
|
|
||||||
ExpectedURL = "https://example.com/endpoint-name?event=RESOLVED&description=alert-description"
|
|
||||||
ExpectedBody = "endpoint-name,alert-description,RESOLVED"
|
|
||||||
)
|
|
||||||
customAlertProvider := &AlertProvider{
|
customAlertProvider := &AlertProvider{
|
||||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]",
|
||||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||||
Headers: nil,
|
|
||||||
}
|
}
|
||||||
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true)
|
alertDescription := "alert-description"
|
||||||
if request.URL.String() != ExpectedURL {
|
scenarios := []struct {
|
||||||
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String())
|
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)
|
for _, scenario := range scenarios {
|
||||||
if string(body) != ExpectedBody {
|
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
t.Error("expected body to be", ExpectedBody, "was", string(body))
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
}
|
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
||||||
}
|
&alert.Alert{Description: &alertDescription},
|
||||||
|
scenario.Resolved,
|
||||||
func TestAlertProvider_buildHTTPRequestWhenTriggered(t *testing.T) {
|
)
|
||||||
const (
|
if request.URL.String() != scenario.ExpectedURL {
|
||||||
ExpectedURL = "https://example.com/endpoint-name?event=TRIGGERED&description=alert-description"
|
t.Error("expected URL to be", scenario.ExpectedURL, "got", request.URL.String())
|
||||||
ExpectedBody = "endpoint-name,alert-description,TRIGGERED"
|
}
|
||||||
)
|
body, _ := io.ReadAll(request.Body)
|
||||||
customAlertProvider := &AlertProvider{
|
if string(body) != scenario.ExpectedBody {
|
||||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
t.Error("expected body to be", scenario.ExpectedBody, "got", string(body))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
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{
|
customAlertProvider := &AlertProvider{
|
||||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||||
Headers: nil,
|
Headers: nil,
|
||||||
Placeholders: map[string]map[string]string{
|
Placeholders: map[string]map[string]string{
|
||||||
"ALERT_TRIGGERED_OR_RESOLVED": {
|
"ALERT_TRIGGERED_OR_RESOLVED": {
|
||||||
"RESOLVED": "test",
|
"RESOLVED": "fixed",
|
||||||
|
"TRIGGERED": "boom",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true)
|
alertDescription := "alert-description"
|
||||||
if request.URL.String() != ExpectedURL {
|
scenarios := []struct {
|
||||||
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String())
|
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)
|
for _, scenario := range scenarios {
|
||||||
if string(body) != ExpectedBody {
|
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
t.Error("expected body to be", ExpectedBody, "was", string(body))
|
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) {
|
func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) {
|
||||||
customAlertProvider := &AlertProvider{
|
customAlertProvider := &AlertProvider{
|
||||||
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
|
||||||
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
|
||||||
Headers: nil,
|
|
||||||
Placeholders: nil,
|
|
||||||
}
|
}
|
||||||
if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" {
|
if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" {
|
||||||
t.Error("expected RESOLVED, got", customAlertProvider.GetAlertStatePlaceholderValue(true))
|
t.Error("expected RESOLVED, got", customAlertProvider.GetAlertStatePlaceholderValue(true))
|
||||||
@@ -187,26 +224,3 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be nil")
|
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"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Discord
|
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||||
@@ -17,17 +17,35 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// 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
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
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
|
return len(provider.WebhookURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -86,6 +104,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}`, message, description, colorCode, results)
|
}`, 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
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
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) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
defer client.InjectHTTPClient(nil)
|
defer client.InjectHTTPClient(nil)
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
@@ -156,3 +193,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be 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.getWebhookURLForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
gomail "gopkg.in/mail.v2"
|
gomail "gopkg.in/mail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,10 +21,29 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// 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
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
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
|
return len(provider.From) > 0 && len(provider.Password) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +58,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", provider.From)
|
m.SetHeader("From", provider.From)
|
||||||
m.SetHeader("To", strings.Split(provider.To, ",")...)
|
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
|
||||||
m.SetHeader("Subject", subject)
|
m.SetHeader("Subject", subject)
|
||||||
m.SetBody("text/plain", body)
|
m.SetBody("text/plain", body)
|
||||||
d := gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
|
d := gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
|
||||||
@@ -72,6 +91,18 @@ func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoin
|
|||||||
return subject, message + description + "\n\nCondition results:\n" + results
|
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
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package email
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{}
|
invalidProvider := AlertProvider{}
|
||||||
if invalidProvider.IsValid() {
|
if invalidProvider.IsValid() {
|
||||||
t.Error("provider shouldn't have been valid")
|
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) {
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
secondDescription := "description-2"
|
secondDescription := "description-2"
|
||||||
@@ -77,3 +118,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be nil")
|
t.Error("expected default alert to be nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_getToForGroup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
InputGroup string
|
||||||
|
ExpectedOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: "to@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: "to@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
To: "to01@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: "to@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-group-should-override",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
To: "to01@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: "to01@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
if got := tt.Provider.getToForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||||
|
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
||||||
@@ -20,6 +20,15 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// 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
|
// IsValid returns whether the provider's configuration is valid
|
||||||
@@ -27,13 +36,24 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
if provider.ClientConfig == nil {
|
if provider.ClientConfig == nil {
|
||||||
provider.ClientConfig = client.GetDefaultConfig()
|
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
|
return len(provider.WebhookURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -118,6 +138,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}`, endpoint.Name, endpoint.Group, message, description, results, endpoint.URL)
|
}`, 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
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{WebhookURL: ""}
|
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||||
if invalidProvider.IsValid() {
|
if invalidProvider.IsValid() {
|
||||||
t.Error("provider shouldn't have been valid")
|
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) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
defer client.InjectHTTPClient(nil)
|
defer client.InjectHTTPClient(nil)
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
@@ -158,3 +195,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package matrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 Matrix
|
||||||
|
type AlertProvider struct {
|
||||||
|
MatrixProviderConfig `yaml:",inline"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
|
MatrixProviderConfig `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHomeserverURL = "https://matrix-client.matrix.org"
|
||||||
|
|
||||||
|
type MatrixProviderConfig struct {
|
||||||
|
// ServerURL is the custom homeserver to use (optional)
|
||||||
|
ServerURL string `yaml:"server-url"`
|
||||||
|
|
||||||
|
// AccessToken is the bot user's access token to send messages
|
||||||
|
AccessToken string `yaml:"access-token"`
|
||||||
|
|
||||||
|
// InternalRoomID is the room that the bot user has permissions to send messages to
|
||||||
|
InternalRoomID string `yaml:"internal-room-id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
registeredGroups := make(map[string]bool)
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.AccessToken) == 0 || len(override.InternalRoomID) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
registeredGroups[override.Group] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(provider.AccessToken) > 0 && len(provider.InternalRoomID) > 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)))
|
||||||
|
config := provider.getConfigForGroup(endpoint.Group)
|
||||||
|
if config.ServerURL == "" {
|
||||||
|
config.ServerURL = defaultHomeserverURL
|
||||||
|
}
|
||||||
|
// The Matrix endpoint requires a unique transaction ID for each event sent
|
||||||
|
txnId := randStringBytes(24)
|
||||||
|
request, err := http.NewRequest(
|
||||||
|
http.MethodPut,
|
||||||
|
fmt.Sprintf("%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s?access_token=%s",
|
||||||
|
config.ServerURL,
|
||||||
|
url.PathEscape(config.InternalRoomID),
|
||||||
|
txnId,
|
||||||
|
url.QueryEscape(config.AccessToken),
|
||||||
|
),
|
||||||
|
buffer,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
response, err := client.GetHTTPClient(nil).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return fmt.Sprintf(`{
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"body": "%s",
|
||||||
|
"formatted_body": "%s"
|
||||||
|
}`,
|
||||||
|
buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
||||||
|
buildHTMLMessageBody(endpoint, alert, result, resolved),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
||||||
|
func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||||
|
var message, results string
|
||||||
|
if resolved {
|
||||||
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
var prefix string
|
||||||
|
if conditionResult.Success {
|
||||||
|
prefix = "✓"
|
||||||
|
} else {
|
||||||
|
prefix = "✕"
|
||||||
|
}
|
||||||
|
results += fmt.Sprintf("\\n%s - %s", prefix, conditionResult.Condition)
|
||||||
|
}
|
||||||
|
var description string
|
||||||
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
|
description = "\\n" + alertDescription
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s\\n%s", message, description, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHTMLMessageBody builds the message body in HTML to include in request
|
||||||
|
func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||||
|
var message, results string
|
||||||
|
if resolved {
|
||||||
|
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
var prefix string
|
||||||
|
if conditionResult.Success {
|
||||||
|
prefix = "✅"
|
||||||
|
} else {
|
||||||
|
prefix = "❌"
|
||||||
|
}
|
||||||
|
results += fmt.Sprintf("<li>%s - <code>%s</code></li>", prefix, conditionResult.Condition)
|
||||||
|
}
|
||||||
|
var description string
|
||||||
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
|
description = fmt.Sprintf("\\n<blockquote>%s</blockquote>", alertDescription)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<h3>%s</h3>%s\\n<h5>Condition results</h5><ul>%s</ul>", message, description, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigForGroup returns the appropriate configuration for a given group
|
||||||
|
func (provider *AlertProvider) getConfigForGroup(group string) MatrixProviderConfig {
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if group == override.Group {
|
||||||
|
return override.MatrixProviderConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.MatrixProviderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func randStringBytes(n int) string {
|
||||||
|
// All the compatible characters to use in a transaction ID
|
||||||
|
const availableCharacterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
b := make([]byte, n)
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
for i := range b {
|
||||||
|
b[i] = availableCharacterBytes[rand.Intn(len(availableCharacterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package matrix
|
||||||
|
|
||||||
|
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 TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
AccessToken: "",
|
||||||
|
InternalRoomID: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
validProvider := AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
validProviderWithHomeserver := AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !validProviderWithHomeserver.IsValid() {
|
||||||
|
t.Error("provider with homeserver should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||||
|
providerWithInvalidOverrideGroup := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "",
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
AccessToken: "",
|
||||||
|
InternalRoomID: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideGroup.IsValid() {
|
||||||
|
t.Error("provider Group shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithInvalidOverrideTo := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
AccessToken: "",
|
||||||
|
InternalRoomID: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideTo.IsValid() {
|
||||||
|
t.Error("provider integration key shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithValidOverride := AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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"},
|
||||||
|
&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\t\"msgtype\": \"m.text\",\n\t\"format\": \"org.matrix.custom.html\",\n\t\"body\": \"An alert for `endpoint-name` has been triggered due to having failed 3 time(s) in a row\\ndescription-1\\n\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\n\t\"formatted_body\": \"<h3>An alert for <code>endpoint-name</code> has been triggered due to having failed 3 time(s) in a row</h3>\\n<blockquote>description-1</blockquote>\\n<h5>Condition results</h5><ul><li>❌ - <code>[CONNECTED] == true</code></li><li>❌ - <code>[STATUS] == 200</code></li></ul>\"\n}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: "{\n\t\"msgtype\": \"m.text\",\n\t\"format\": \"org.matrix.custom.html\",\n\t\"body\": \"An alert for `endpoint-name` has been resolved after passing successfully 5 time(s) in a row\\ndescription-2\\n\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\n\t\"formatted_body\": \"<h3>An alert for <code>endpoint-name</code> has been resolved after passing successfully 5 time(s) in a row</h3>\\n<blockquote>description-2</blockquote>\\n<h5>Condition results</h5><ul><li>✅ - <code>[CONNECTED] == true</code></li><li>✅ - <code>[STATUS] == 200</code></li></ul>\"\n}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if body != scenario.ExpectedBody {
|
||||||
|
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||||
|
}
|
||||||
|
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_getConfigForGroup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
InputGroup string
|
||||||
|
ExpectedOutput MatrixProviderConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example01.com",
|
||||||
|
AccessToken: "12",
|
||||||
|
InternalRoomID: "!a:example01.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-group-should-override",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
AccessToken: "1",
|
||||||
|
InternalRoomID: "!a:example.com",
|
||||||
|
},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
MatrixProviderConfig: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example01.com",
|
||||||
|
AccessToken: "12",
|
||||||
|
InternalRoomID: "!a:example01.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: MatrixProviderConfig{
|
||||||
|
ServerURL: "https://example01.com",
|
||||||
|
AccessToken: "12",
|
||||||
|
InternalRoomID: "!a:example01.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
if got := tt.Provider.getConfigForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||||
|
t.Errorf("AlertProvider.getConfigForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
||||||
@@ -20,6 +20,15 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// 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
|
// IsValid returns whether the provider's configuration is valid
|
||||||
@@ -27,13 +36,22 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
if provider.ClientConfig == nil {
|
if provider.ClientConfig == nil {
|
||||||
provider.ClientConfig = client.GetDefaultConfig()
|
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
|
return len(provider.WebhookURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -101,6 +119,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}`, message, message, description, color, endpoint.URL, results)
|
}`, 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
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
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) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
defer client.InjectHTTPClient(nil)
|
defer client.InjectHTTPClient(nil)
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
@@ -156,3 +197,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package messagebird
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,7 +34,7 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -51,19 +52,26 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Body struct {
|
||||||
|
Originator string `json:"originator"`
|
||||||
|
Recipients string `json:"recipients"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(`{
|
body, _ := json.Marshal(Body{
|
||||||
"originator": "%s",
|
Originator: provider.Originator,
|
||||||
"recipients": "%s",
|
Recipients: provider.Recipients,
|
||||||
"body": "%s"
|
Body: message,
|
||||||
}`, provider.Originator, provider.Recipients, message)
|
})
|
||||||
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessagebirdAlertProvider_IsValid(t *testing.T) {
|
func TestMessagebirdAlertProvider_IsValid(t *testing.T) {
|
||||||
@@ -118,14 +118,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Provider: AlertProvider{AccessKey: "1", Originator: "2", Recipients: "3"},
|
Provider: AlertProvider{AccessKey: "1", Originator: "2", Recipients: "3"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\n \"originator\": \"2\",\n \"recipients\": \"3\",\n \"body\": \"TRIGGERED: endpoint-name - description-1\"\n}",
|
ExpectedBody: "{\"originator\":\"2\",\"recipients\":\"3\",\"body\":\"TRIGGERED: endpoint-name - description-1\"}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{AccessKey: "4", Originator: "5", Recipients: "6"},
|
Provider: AlertProvider{AccessKey: "4", Originator: "5", Recipients: "6"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\n \"originator\": \"5\",\n \"recipients\": \"6\",\n \"body\": \"RESOLVED: endpoint-name - description-2\"\n}",
|
ExpectedBody: "{\"originator\":\"5\",\"recipients\":\"6\",\"body\":\"RESOLVED: endpoint-name - description-2\"}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
@@ -141,7 +141,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if body != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||||
}
|
}
|
||||||
out := make(map[string]interface{})
|
out := make(map[string]interface{})
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package ntfy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v4/client"
|
||||||
|
"github.com/TwiN/gatus/v4/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultURL = "https://ntfy.sh"
|
||||||
|
DefaultPriority = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||||
|
type AlertProvider struct {
|
||||||
|
Topic string `yaml:"topic"`
|
||||||
|
URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
|
||||||
|
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
|
||||||
|
|
||||||
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
if len(provider.URL) == 0 {
|
||||||
|
provider.URL = DefaultURL
|
||||||
|
}
|
||||||
|
if provider.Priority == 0 {
|
||||||
|
provider.Priority = DefaultPriority
|
||||||
|
}
|
||||||
|
return len(provider.URL) > 0 && len(provider.Topic) > 0 && provider.Priority > 0 && provider.Priority < 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.URL, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
response, err := client.GetHTTPClient(nil).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.StatusCode > 399 {
|
||||||
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Body struct {
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRequestBody builds the request body for the provider
|
||||||
|
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||||
|
var message, tag string
|
||||||
|
if len(alert.GetDescription()) > 0 {
|
||||||
|
message = endpoint.DisplayName() + " - " + alert.GetDescription()
|
||||||
|
} else {
|
||||||
|
message = endpoint.DisplayName()
|
||||||
|
}
|
||||||
|
if resolved {
|
||||||
|
tag = "white_check_mark"
|
||||||
|
} else {
|
||||||
|
tag = "x"
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(Body{
|
||||||
|
Topic: provider.Topic,
|
||||||
|
Title: "Gatus",
|
||||||
|
Message: message,
|
||||||
|
Tags: []string{tag},
|
||||||
|
Priority: provider.Priority,
|
||||||
|
})
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package ntfy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v4/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
provider AlertProvider
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-url-should-use-default-value",
|
||||||
|
provider: AlertProvider{Topic: "example", Priority: 1},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-topic",
|
||||||
|
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "", Priority: 1},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-priority-too-high",
|
||||||
|
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 6},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-priority-too-low",
|
||||||
|
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: -1},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-priority-should-use-default-value",
|
||||||
|
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example"},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
if scenario.provider.IsValid() != scenario.expected {
|
||||||
|
t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.IsValid())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
ExpectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: "{\"topic\":\"example\",\"title\":\"Gatus\",\"message\":\"endpoint-name - description-1\",\"tags\":[\"x\"],\"priority\":1}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: "{\"topic\":\"example\",\"title\":\"Gatus\",\"message\":\"endpoint-name - description-2\",\"tags\":[\"white_check_mark\"],\"priority\":2}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||||
|
}
|
||||||
|
out := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(body, &out); err != nil {
|
||||||
|
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
"github.com/TwiN/gatus/v4/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
"github.com/TwiN/gatus/v4/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/email"
|
"github.com/TwiN/gatus/v4/alerting/provider/email"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v4/alerting/provider/googlechat"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v4/alerting/provider/matrix"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v4/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v4/alerting/provider/messagebird"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/slack"
|
"github.com/TwiN/gatus/v4/alerting/provider/ntfy"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/teams"
|
"github.com/TwiN/gatus/v4/alerting/provider/opsgenie"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v4/alerting/provider/pagerduty"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v4/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"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
|
// AlertProvider is the interface that each providers should implement
|
||||||
@@ -56,8 +59,11 @@ var (
|
|||||||
_ AlertProvider = (*discord.AlertProvider)(nil)
|
_ AlertProvider = (*discord.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*email.AlertProvider)(nil)
|
_ AlertProvider = (*email.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*ntfy.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*opsgenie.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
|
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*slack.AlertProvider)(nil)
|
_ AlertProvider = (*slack.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*teams.AlertProvider)(nil)
|
_ AlertProvider = (*teams.AlertProvider)(nil)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseWithDefaultAlert(t *testing.T) {
|
func TestParseWithDefaultAlert(t *testing.T) {
|
||||||
|
|||||||
@@ -6,28 +6,44 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Slack
|
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||||
type AlertProvider struct {
|
type AlertProvider struct {
|
||||||
WebhookURL string `yaml:"webhook-url"` // Slack webhook URL
|
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 is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
// 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
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
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
|
return len(provider.WebhookURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -86,6 +102,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}`, message, description, color, results)
|
}`, 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
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{WebhookURL: ""}
|
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||||
if invalidProvider.IsValid() {
|
if invalidProvider.IsValid() {
|
||||||
t.Error("provider shouldn't have been valid")
|
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) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
defer client.InjectHTTPClient(nil)
|
defer client.InjectHTTPClient(nil)
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
@@ -79,7 +116,7 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "name"},
|
&core.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&core.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*core.ConditionResult{
|
||||||
@@ -175,3 +212,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be 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.getWebhookURLForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Teams
|
// AlertProvider is the configuration necessary for sending an alert using Teams
|
||||||
@@ -17,17 +17,35 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// 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
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
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
|
return len(provider.WebhookURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -86,6 +104,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}`, color, message, description, endpoint.URL, results)
|
}`, 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
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{WebhookURL: ""}
|
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||||
if invalidProvider.IsValid() {
|
if invalidProvider.IsValid() {
|
||||||
t.Error("provider shouldn't have been valid")
|
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) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
defer client.InjectHTTPClient(nil)
|
defer client.InjectHTTPClient(nil)
|
||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
@@ -156,3 +193,66 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
|||||||
t.Error("expected default alert to be 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,15 +6,21 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultAPIURL = "https://api.telegram.org"
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Telegram
|
// AlertProvider is the configuration necessary for sending an alert using Telegram
|
||||||
type AlertProvider struct {
|
type AlertProvider struct {
|
||||||
Token string `yaml:"token"`
|
Token string `yaml:"token"`
|
||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
|
APIURL string `yaml:"api-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 is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
@@ -22,18 +28,25 @@ type AlertProvider struct {
|
|||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
func (provider *AlertProvider) IsValid() bool {
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
if provider.ClientConfig == nil {
|
||||||
|
provider.ClientConfig = client.GetDefaultConfig()
|
||||||
|
}
|
||||||
return len(provider.Token) > 0 && len(provider.ID) > 0
|
return len(provider.Token) > 0 && len(provider.ID) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
request.Header.Set("Content-Type", "application/json")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
response, err := client.GetHTTPClient(nil).Do(request)
|
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,31 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/test"
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{Token: "", ID: ""}
|
t.Run("invalid-provider", func(t *testing.T) {
|
||||||
if invalidProvider.IsValid() {
|
invalidProvider := AlertProvider{Token: "", ID: ""}
|
||||||
t.Error("provider shouldn't have been valid")
|
if invalidProvider.IsValid() {
|
||||||
}
|
t.Error("provider shouldn't have been valid")
|
||||||
validProvider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
|
}
|
||||||
if !validProvider.IsValid() {
|
})
|
||||||
t.Error("provider should've been valid")
|
t.Run("valid-provider", func(t *testing.T) {
|
||||||
}
|
validProvider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
|
||||||
|
if validProvider.ClientConfig != nil {
|
||||||
|
t.Error("provider client config should have been nil prior to IsValid() being executed")
|
||||||
|
}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
if validProvider.ClientConfig == nil {
|
||||||
|
t.Error("provider client config should have been set after IsValid() was executed")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertProvider_Send(t *testing.T) {
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package twilio
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
|||||||
// injectedHTTPClient is used for testing purposes
|
// injectedHTTPClient is used for testing purposes
|
||||||
var injectedHTTPClient *http.Client
|
var injectedHTTPClient *http.Client
|
||||||
|
|
||||||
// GetHTTPClient returns the shared HTTP client
|
// GetHTTPClient returns the shared HTTP client, or the client from the configuration passed
|
||||||
func GetHTTPClient(config *Config) *http.Client {
|
func GetHTTPClient(config *Config) *http.Client {
|
||||||
if injectedHTTPClient != nil {
|
if injectedHTTPClient != nil {
|
||||||
return injectedHTTPClient
|
return injectedHTTPClient
|
||||||
|
|||||||
+74
-1
@@ -1,8 +1,13 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetHTTPClient(t *testing.T) {
|
func TestGetHTTPClient(t *testing.T) {
|
||||||
@@ -10,8 +15,18 @@ func TestGetHTTPClient(t *testing.T) {
|
|||||||
Insecure: false,
|
Insecure: false,
|
||||||
IgnoreRedirect: false,
|
IgnoreRedirect: false,
|
||||||
Timeout: 0,
|
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 {
|
if GetHTTPClient(cfg) == nil {
|
||||||
t.Error("expected client to not be 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")
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+130
-4
@@ -1,9 +1,18 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -11,7 +20,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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{
|
defaultConfig = Config{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
IgnoreRedirect: false,
|
IgnoreRedirect: false,
|
||||||
@@ -28,22 +40,99 @@ func GetDefaultConfig() *Config {
|
|||||||
// Config is the configuration for clients
|
// Config is the configuration for clients
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
||||||
Insecure bool `yaml:"insecure"`
|
Insecure bool `yaml:"insecure,omitempty"`
|
||||||
|
|
||||||
// IgnoreRedirect determines whether to ignore redirects (true) or follow them (false, default)
|
// 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 for the client
|
||||||
Timeout time.Duration `yaml:"timeout"`
|
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
|
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
|
// 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 {
|
if c.Timeout < time.Millisecond {
|
||||||
c.Timeout = 10 * time.Second
|
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.
|
// GetHTTPClient return an HTTP client matching the Config's parameters.
|
||||||
@@ -68,6 +157,43 @@ func (c *Config) getHTTPClient() *http.Client {
|
|||||||
return nil
|
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
|
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")
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
-1
@@ -31,7 +31,7 @@ endpoints:
|
|||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: example-dns-query
|
- 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
|
interval: 5m
|
||||||
dns:
|
dns:
|
||||||
query-name: "example.com"
|
query-name: "example.com"
|
||||||
@@ -45,3 +45,9 @@ endpoints:
|
|||||||
interval: 1m
|
interval: 1m
|
||||||
conditions:
|
conditions:
|
||||||
- "[CONNECTED] == true"
|
- "[CONNECTED] == true"
|
||||||
|
|
||||||
|
- name: check-domain-expiration
|
||||||
|
url: "https://example.org/"
|
||||||
|
interval: 1h
|
||||||
|
conditions:
|
||||||
|
- "[DOMAIN_EXPIRATION] > 720h"
|
||||||
|
|||||||
+42
-10
@@ -2,19 +2,22 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting"
|
"github.com/TwiN/gatus/v4/alerting"
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider"
|
"github.com/TwiN/gatus/v4/alerting/provider"
|
||||||
"github.com/TwiN/gatus/v3/config/maintenance"
|
"github.com/TwiN/gatus/v4/config/maintenance"
|
||||||
"github.com/TwiN/gatus/v3/config/ui"
|
"github.com/TwiN/gatus/v4/config/remote"
|
||||||
"github.com/TwiN/gatus/v3/config/web"
|
"github.com/TwiN/gatus/v4/config/ui"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/config/web"
|
||||||
"github.com/TwiN/gatus/v3/security"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/storage"
|
"github.com/TwiN/gatus/v4/security"
|
||||||
|
"github.com/TwiN/gatus/v4/storage"
|
||||||
|
"github.com/TwiN/gatus/v4/util"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,10 +88,24 @@ type Config struct {
|
|||||||
// Maintenance is the configuration for creating a maintenance window in which no alerts are sent
|
// Maintenance is the configuration for creating a maintenance window in which no alerts are sent
|
||||||
Maintenance *maintenance.Config `yaml:"maintenance,omitempty"`
|
Maintenance *maintenance.Config `yaml:"maintenance,omitempty"`
|
||||||
|
|
||||||
|
// Remote is the configuration for remote Gatus instances
|
||||||
|
// WARNING: This is in ALPHA and may change or be completely removed in the future
|
||||||
|
Remote *remote.Config `yaml:"remote,omitempty"`
|
||||||
|
|
||||||
filePath string // path to the file from which config was loaded from
|
filePath string // path to the file from which config was loaded from
|
||||||
lastFileModTime time.Time // last modification time
|
lastFileModTime time.Time // last modification time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
|
||||||
|
for i := 0; i < len(config.Endpoints); i++ {
|
||||||
|
ep := config.Endpoints[i]
|
||||||
|
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// HasLoadedConfigurationFileBeenModified returns whether the file that the
|
// HasLoadedConfigurationFileBeenModified returns whether the file that the
|
||||||
// configuration has been loaded from has been modified since it was last read
|
// configuration has been loaded from has been modified since it was last read
|
||||||
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
|
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
|
||||||
@@ -185,10 +202,22 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
if err := validateStorageConfig(config); err != nil {
|
if err := validateStorageConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := validateRemoteConfig(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateRemoteConfig(config *Config) error {
|
||||||
|
if config.Remote != nil {
|
||||||
|
if err := config.Remote.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateStorageConfig(config *Config) error {
|
func validateStorageConfig(config *Config) error {
|
||||||
if config.Storage == nil {
|
if config.Storage == nil {
|
||||||
config.Storage = &storage.Config{
|
config.Storage = &storage.Config{
|
||||||
@@ -239,7 +268,7 @@ func validateEndpointsConfig(config *Config) error {
|
|||||||
log.Printf("[config][validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name)
|
log.Printf("[config][validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name)
|
||||||
}
|
}
|
||||||
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return fmt.Errorf("invalid endpoint %s: %s", endpoint.DisplayName(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config][validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
log.Printf("[config][validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
||||||
@@ -273,9 +302,12 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
alertTypes := []alert.Type{
|
alertTypes := []alert.Type{
|
||||||
alert.TypeCustom,
|
alert.TypeCustom,
|
||||||
alert.TypeDiscord,
|
alert.TypeDiscord,
|
||||||
|
alert.TypeGoogleChat,
|
||||||
alert.TypeEmail,
|
alert.TypeEmail,
|
||||||
|
alert.TypeMatrix,
|
||||||
alert.TypeMattermost,
|
alert.TypeMattermost,
|
||||||
alert.TypeMessagebird,
|
alert.TypeMessagebird,
|
||||||
|
alert.TypeNtfy,
|
||||||
alert.TypeOpsgenie,
|
alert.TypeOpsgenie,
|
||||||
alert.TypePagerDuty,
|
alert.TypePagerDuty,
|
||||||
alert.TypeSlack,
|
alert.TypeSlack,
|
||||||
|
|||||||
+27
-75
@@ -5,22 +5,21 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting"
|
"github.com/TwiN/gatus/v4/alerting"
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
"github.com/TwiN/gatus/v4/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
"github.com/TwiN/gatus/v4/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v4/alerting/provider/mattermost"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
"github.com/TwiN/gatus/v4/alerting/provider/messagebird"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
"github.com/TwiN/gatus/v4/alerting/provider/pagerduty"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/slack"
|
"github.com/TwiN/gatus/v4/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/teams"
|
"github.com/TwiN/gatus/v4/alerting/provider/teams"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v4/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v3/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v4/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/config/ui"
|
"github.com/TwiN/gatus/v4/config/web"
|
||||||
"github.com/TwiN/gatus/v3/config/web"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/storage"
|
||||||
"github.com/TwiN/gatus/v3/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadFileThatDoesNotExist(t *testing.T) {
|
func TestLoadFileThatDoesNotExist(t *testing.T) {
|
||||||
@@ -39,10 +38,6 @@ func TestLoadDefaultConfigurationFile(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseAndValidateConfigBytes(t *testing.T) {
|
func TestParseAndValidateConfigBytes(t *testing.T) {
|
||||||
file := t.TempDir() + "/test.db"
|
file := t.TempDir() + "/test.db"
|
||||||
ui.StaticFolder = "../web/static"
|
|
||||||
defer func() {
|
|
||||||
ui.StaticFolder = "./web/static"
|
|
||||||
}()
|
|
||||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||||
storage:
|
storage:
|
||||||
type: sqlite
|
type: sqlite
|
||||||
@@ -53,7 +48,14 @@ maintenance:
|
|||||||
duration: 4h
|
duration: 4h
|
||||||
every: [Monday, Thursday]
|
every: [Monday, Thursday]
|
||||||
ui:
|
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:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health
|
url: https://twin.sh/health
|
||||||
@@ -88,8 +90,8 @@ endpoints:
|
|||||||
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite {
|
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)
|
t.Error("expected storage to be set to sqlite, got", config.Storage)
|
||||||
}
|
}
|
||||||
if config.UI == nil || config.UI.Title != "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 Config.UI.Title to be Test")
|
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 {
|
if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 {
|
||||||
t.Error("Expected Config.Maintenance to be configured properly")
|
t.Error("Expected Config.Maintenance to be configured properly")
|
||||||
@@ -1118,7 +1120,7 @@ endpoints:
|
|||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
if err != core.ErrEndpointWithNoName {
|
if err == nil {
|
||||||
t.Error("should've returned an error")
|
t.Error("should've returned an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1203,7 +1205,7 @@ endpoints:
|
|||||||
t.Errorf("config.Security.Basic.Username should've been %s, but was %s", expectedUsername, config.Security.Basic.Username)
|
t.Errorf("config.Security.Basic.Username should've been %s, but was %s", expectedUsername, config.Security.Basic.Username)
|
||||||
}
|
}
|
||||||
if config.Security.Basic.PasswordBcryptHashBase64Encoded != expectedPasswordHash {
|
if config.Security.Basic.PasswordBcryptHashBase64Encoded != expectedPasswordHash {
|
||||||
t.Errorf("config.Security.Basic.PasswordBcryptHashBase64Encoded should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordSha512Hash)
|
t.Errorf("config.Security.Basic.PasswordBcryptHashBase64Encoded should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordBcryptHashBase64Encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1302,53 +1304,3 @@ endpoints:
|
|||||||
t.Error("services should've been merged in 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,
|
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",
|
name: "every-monday-at-0000",
|
||||||
cfg: &Config{
|
cfg: &Config{
|
||||||
@@ -168,6 +177,24 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: true,
|
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",
|
name: "under-maintenance-starting-4h-ago-for-8h",
|
||||||
cfg: &Config{
|
cfg: &Config{
|
||||||
@@ -176,6 +203,14 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: true,
|
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",
|
name: "under-maintenance-starting-4h-ago-for-3h",
|
||||||
cfg: &Config{
|
cfg: &Config{
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v4/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTICE: This is an experimental alpha feature and may be updated/removed in future versions.
|
||||||
|
// For more information, see https://github.com/TwiN/gatus/issues/64
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Instances is a list of remote instances to retrieve endpoint statuses from.
|
||||||
|
Instances []Instance `yaml:"instances,omitempty"`
|
||||||
|
|
||||||
|
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||||
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
EndpointPrefix string `yaml:"endpoint-prefix"`
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ValidateAndSetDefaults() error {
|
||||||
|
if c.ClientConfig == nil {
|
||||||
|
c.ClientConfig = client.GetDefaultConfig()
|
||||||
|
} else {
|
||||||
|
if err := c.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(c.Instances) > 0 {
|
||||||
|
log.Println("WARNING: Your configuration is using 'remote', which is in alpha and may be updated/removed in future versions.")
|
||||||
|
log.Println("WARNING: See https://github.com/TwiN/gatus/issues/64 for more information")
|
||||||
|
log.Println("WARNING: This feature is a candidate for removal in future versions. Please comment on the issue above if you need this feature.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+30
-8
@@ -2,7 +2,10 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v4/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -13,17 +16,30 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// StaticFolder is the path to the location of the static folder from the root path of the project
|
ErrButtonValidationFailed = errors.New("invalid button configuration: missing required name or link")
|
||||||
// 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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the configuration for the UI of Gatus
|
// Config is the configuration for the UI of Gatus
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Title string `yaml:"title,omitempty"` // Title of the page
|
Title string `yaml:"title,omitempty"` // Title of the page
|
||||||
Header string `yaml:"header,omitempty"` // Header is the text at the top 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
|
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
|
||||||
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
|
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
|
// GetDefaultConfig returns a Config struct with the default values
|
||||||
@@ -47,7 +63,13 @@ func (cfg *Config) ValidateAndSetDefaults() error {
|
|||||||
if len(cfg.Header) == 0 {
|
if len(cfg.Header) == 0 {
|
||||||
cfg.Header = defaultLink
|
cfg.Header = defaultLink
|
||||||
}
|
}
|
||||||
t, err := template.ParseFiles(StaticFolder + "/index.html")
|
for _, btn := range cfg.Buttons {
|
||||||
|
if err := btn.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate that the template works
|
||||||
|
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-4
@@ -1,14 +1,11 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||||
StaticFolder = "../../web/static"
|
|
||||||
defer func() {
|
|
||||||
StaticFolder = "./web/static"
|
|
||||||
}()
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Title: "",
|
Title: "",
|
||||||
Header: "",
|
Header: "",
|
||||||
@@ -26,6 +23,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) {
|
func TestGetDefaultConfig(t *testing.T) {
|
||||||
defaultConfig := GetDefaultConfig()
|
defaultConfig := GetDefaultConfig()
|
||||||
if defaultConfig.Title != defaultTitle {
|
if defaultConfig.Title != defaultTitle {
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config/ui"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/config/web"
|
"github.com/TwiN/gatus/v4/controller/handler"
|
||||||
"github.com/TwiN/gatus/v3/controller/handler"
|
|
||||||
"github.com/TwiN/gatus/v3/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -21,19 +19,19 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Handle creates the router and starts the server
|
// Handle creates the router and starts the server
|
||||||
func Handle(securityConfig *security.Config, webConfig *web.Config, uiConfig *ui.Config, enableMetrics bool) {
|
func Handle(cfg *config.Config) {
|
||||||
var router http.Handler = handler.CreateRouter(ui.StaticFolder, securityConfig, uiConfig, enableMetrics)
|
var router http.Handler = handler.CreateRouter(cfg)
|
||||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||||
router = handler.DevelopmentCORS(router)
|
router = handler.DevelopmentCORS(router)
|
||||||
}
|
}
|
||||||
server = &http.Server{
|
server = &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%d", webConfig.Address, webConfig.Port),
|
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
|
||||||
Handler: router,
|
Handler: router,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: 15 * time.Second,
|
||||||
IdleTimeout: 15 * time.Second,
|
IdleTimeout: 15 * time.Second,
|
||||||
}
|
}
|
||||||
log.Println("[controller][Handle] Listening on " + webConfig.SocketAddress())
|
log.Println("[controller][Handle] Listening on " + cfg.Web.SocketAddress())
|
||||||
if os.Getenv("ROUTER_TEST") == "true" {
|
if os.Getenv("ROUTER_TEST") == "true" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/config/web"
|
"github.com/TwiN/gatus/v4/config/web"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandle(t *testing.T) {
|
func TestHandle(t *testing.T) {
|
||||||
@@ -32,7 +32,7 @@ func TestHandle(t *testing.T) {
|
|||||||
_ = os.Setenv("ROUTER_TEST", "true")
|
_ = os.Setenv("ROUTER_TEST", "true")
|
||||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||||
defer os.Clearenv()
|
defer os.Clearenv()
|
||||||
Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics)
|
Handle(cfg)
|
||||||
defer Shutdown()
|
defer Shutdown()
|
||||||
request, _ := http.NewRequest("GET", "/health", http.NoBody)
|
request, _ := http.NewRequest("GET", "/health", http.NoBody)
|
||||||
responseRecorder := httptest.NewRecorder()
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
|||||||
+130
-31
@@ -7,8 +7,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"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"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +23,16 @@ const (
|
|||||||
badgeColorHexVeryBad = "#c7130a"
|
badgeColorHexVeryBad = "#c7130a"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HealthStatusUp = "up"
|
||||||
|
HealthStatusDown = "down"
|
||||||
|
HealthStatusUnknown = "?"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
badgeColors = []string{badgeColorHexAwesome, badgeColorHexGreat, badgeColorHexGood, badgeColorHexPassable, badgeColorHexBad}
|
||||||
|
)
|
||||||
|
|
||||||
// UptimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
// UptimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||||
//
|
//
|
||||||
// Valid values for {duration}: 7d, 24h, 1h
|
// Valid values for {duration}: 7d, 24h, 1h
|
||||||
@@ -61,23 +73,48 @@ func UptimeBadge(writer http.ResponseWriter, request *http.Request) {
|
|||||||
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||||
//
|
//
|
||||||
// Valid values for {duration}: 7d, 24h, 1h
|
// Valid values for {duration}: 7d, 24h, 1h
|
||||||
func ResponseTimeBadge(writer http.ResponseWriter, request *http.Request) {
|
func ResponseTimeBadge(config *config.Config) http.HandlerFunc {
|
||||||
variables := mux.Vars(request)
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
duration := variables["duration"]
|
variables := mux.Vars(request)
|
||||||
var from time.Time
|
duration := variables["duration"]
|
||||||
switch duration {
|
var from time.Time
|
||||||
case "7d":
|
switch duration {
|
||||||
from = time.Now().Add(-7 * 24 * time.Hour)
|
case "7d":
|
||||||
case "24h":
|
from = time.Now().Add(-7 * 24 * time.Hour)
|
||||||
from = time.Now().Add(-24 * time.Hour)
|
case "24h":
|
||||||
case "1h":
|
from = time.Now().Add(-24 * time.Hour)
|
||||||
from = time.Now().Add(-2 * time.Hour) // Because response time metrics are stored by hour, we have to cheat a little
|
case "1h":
|
||||||
default:
|
from = time.Now().Add(-2 * time.Hour) // Because response time metrics are stored by hour, we have to cheat a little
|
||||||
http.Error(writer, "Durations supported: 7d, 24h, 1h", http.StatusBadRequest)
|
default:
|
||||||
return
|
http.Error(writer, "Durations supported: 7d, 24h, 1h", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := variables["key"]
|
||||||
|
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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(generateResponseTimeBadgeSVG(duration, averageResponseTime, key, config))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"]
|
key := variables["key"]
|
||||||
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
pagingConfig := paging.NewEndpointStatusParams()
|
||||||
|
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == common.ErrEndpointNotFound {
|
if err == common.ErrEndpointNotFound {
|
||||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||||
@@ -88,11 +125,19 @@ func ResponseTimeBadge(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
return
|
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("Content-Type", "image/svg+xml")
|
||||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
writer.Header().Set("Expires", "0")
|
writer.Header().Set("Expires", "0")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, _ = writer.Write(generateResponseTimeBadgeSVG(duration, averageResponseTime))
|
_, _ = writer.Write(generateHealthBadgeSVG(healthStatus))
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
||||||
@@ -161,7 +206,7 @@ func getBadgeColorFromUptime(uptime float64) string {
|
|||||||
return badgeColorHexVeryBad
|
return badgeColorHexVeryBad
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []byte {
|
func generateResponseTimeBadgeSVG(duration string, averageResponseTime int, key string, cfg *config.Config) []byte {
|
||||||
var labelWidth, valueWidth int
|
var labelWidth, valueWidth int
|
||||||
switch duration {
|
switch duration {
|
||||||
case "7d":
|
case "7d":
|
||||||
@@ -172,7 +217,7 @@ func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []by
|
|||||||
labelWidth = 105
|
labelWidth = 105
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
color := getBadgeColorFromResponseTime(averageResponseTime)
|
color := getBadgeColorFromResponseTime(averageResponseTime, key, cfg)
|
||||||
sanitizedValue := strconv.Itoa(averageResponseTime) + "ms"
|
sanitizedValue := strconv.Itoa(averageResponseTime) + "ms"
|
||||||
valueWidth = len(sanitizedValue) * 11
|
valueWidth = len(sanitizedValue) * 11
|
||||||
width := labelWidth + valueWidth
|
width := labelWidth + valueWidth
|
||||||
@@ -209,17 +254,71 @@ func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []by
|
|||||||
return svg
|
return svg
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBadgeColorFromResponseTime(responseTime int) string {
|
func getBadgeColorFromResponseTime(responseTime int, key string, cfg *config.Config) string {
|
||||||
if responseTime <= 50 {
|
endpoint := cfg.GetEndpointByKey(key)
|
||||||
return badgeColorHexAwesome
|
// the threshold config requires 5 values, so we can be sure it's set here
|
||||||
} else if responseTime <= 200 {
|
for i := 0; i < 5; i++ {
|
||||||
return badgeColorHexGreat
|
if responseTime <= endpoint.UIConfig.Badge.ResponseTime.Thresholds[i] {
|
||||||
} else if responseTime <= 300 {
|
return badgeColors[i]
|
||||||
return badgeColorHexGood
|
}
|
||||||
} else if responseTime <= 500 {
|
|
||||||
return badgeColorHexPassable
|
|
||||||
} else if responseTime <= 750 {
|
|
||||||
return badgeColorHexBad
|
|
||||||
}
|
}
|
||||||
return badgeColorHexVeryBad
|
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,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/core/ui"
|
||||||
"github.com/TwiN/gatus/v3/watchdog"
|
"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 store.Get().Clear()
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
@@ -29,9 +30,39 @@ 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()})
|
testSuccessfulResult = core.Result{
|
||||||
router := CreateRouter("../../web/static", cfg.Security, nil, cfg.Metrics)
|
Hostname: "example.org",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
HTTPStatus: 200,
|
||||||
|
Errors: nil,
|
||||||
|
Connected: true,
|
||||||
|
Success: true,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Duration: 150 * time.Millisecond,
|
||||||
|
CertificateExpiration: 10 * time.Hour,
|
||||||
|
ConditionResults: []*core.ConditionResult{
|
||||||
|
{
|
||||||
|
Condition: "[STATUS] == 200",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "[RESPONSE_TIME] < 500",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
||||||
|
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
||||||
|
|
||||||
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
|
router := CreateRouter(cfg)
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
@@ -89,20 +120,35 @@ func TestUptimeBadge(t *testing.T) {
|
|||||||
Path: "/api/v1/endpoints/invalid_key/response-times/7d/badge.svg",
|
Path: "/api/v1/endpoints/invalid_key/response-times/7d/badge.svg",
|
||||||
ExpectedCode: http.StatusNotFound,
|
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",
|
Name: "chart-response-time-24h",
|
||||||
Path: "/api/v1/endpoints/core_backend/response-times/24h/chart.svg",
|
Path: "/api/v1/endpoints/core_backend/response-times/24h/chart.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{ // XXX: Remove this in v4.0.0
|
{
|
||||||
Name: "backward-compatible-services-badge-uptime-1h",
|
Name: "chart-response-time-7d",
|
||||||
Path: "/api/v1/services/core_frontend/uptimes/1h/badge.svg",
|
Path: "/api/v1/endpoints/core_frontend/response-times/7d/chart.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{ // XXX: Remove this in v4.0.0
|
{
|
||||||
Name: "backward-compatible-services-chart-response-time-24h",
|
Name: "chart-response-time-with-invalid-duration",
|
||||||
Path: "/api/v1/services/core_backend/response-times/24h/chart.svg",
|
Path: "/api/v1/endpoints/core_backend/response-times/3d/chart.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
@@ -176,55 +222,155 @@ func TestGetBadgeColorFromUptime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||||
|
defer store.Get().Clear()
|
||||||
|
defer cache.Clear()
|
||||||
|
|
||||||
|
var (
|
||||||
|
firstCondition = core.Condition("[STATUS] == 200")
|
||||||
|
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||||
|
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
|
)
|
||||||
|
|
||||||
|
firstTestEndpoint := core.Endpoint{
|
||||||
|
Name: "a",
|
||||||
|
URL: "https://example.org/what/ever",
|
||||||
|
Method: "GET",
|
||||||
|
Body: "body",
|
||||||
|
Interval: 30 * time.Second,
|
||||||
|
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
|
Alerts: nil,
|
||||||
|
NumberOfFailuresInARow: 0,
|
||||||
|
NumberOfSuccessesInARow: 0,
|
||||||
|
UIConfig: ui.GetDefaultConfig(),
|
||||||
|
}
|
||||||
|
secondTestEndpoint := core.Endpoint{
|
||||||
|
Name: "b",
|
||||||
|
URL: "https://example.org/what/ever",
|
||||||
|
Method: "GET",
|
||||||
|
Body: "body",
|
||||||
|
Interval: 30 * time.Second,
|
||||||
|
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
|
Alerts: nil,
|
||||||
|
NumberOfFailuresInARow: 0,
|
||||||
|
NumberOfSuccessesInARow: 0,
|
||||||
|
UIConfig: &ui.Config{
|
||||||
|
Badge: &ui.Badge{
|
||||||
|
ResponseTime: &ui.ResponseTime{
|
||||||
|
Thresholds: []int{100, 500, 1000, 2000, 3000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cfg := &config.Config{
|
||||||
|
Metrics: true,
|
||||||
|
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Get().Insert(&firstTestEndpoint, &testSuccessfulResult)
|
||||||
|
store.Get().Insert(&secondTestEndpoint, &testSuccessfulResult)
|
||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
|
Key string
|
||||||
ResponseTime int
|
ResponseTime int
|
||||||
ExpectedColor string
|
ExpectedColor string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 10,
|
ResponseTime: 10,
|
||||||
ExpectedColor: badgeColorHexAwesome,
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 50,
|
ResponseTime: 50,
|
||||||
ExpectedColor: badgeColorHexAwesome,
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 75,
|
ResponseTime: 75,
|
||||||
ExpectedColor: badgeColorHexGreat,
|
ExpectedColor: badgeColorHexGreat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 150,
|
ResponseTime: 150,
|
||||||
ExpectedColor: badgeColorHexGreat,
|
ExpectedColor: badgeColorHexGreat,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 201,
|
ResponseTime: 201,
|
||||||
ExpectedColor: badgeColorHexGood,
|
ExpectedColor: badgeColorHexGood,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 300,
|
ResponseTime: 300,
|
||||||
ExpectedColor: badgeColorHexGood,
|
ExpectedColor: badgeColorHexGood,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 301,
|
ResponseTime: 301,
|
||||||
ExpectedColor: badgeColorHexPassable,
|
ExpectedColor: badgeColorHexPassable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 450,
|
ResponseTime: 450,
|
||||||
ExpectedColor: badgeColorHexPassable,
|
ExpectedColor: badgeColorHexPassable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 700,
|
ResponseTime: 700,
|
||||||
ExpectedColor: badgeColorHexBad,
|
ExpectedColor: badgeColorHexBad,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Key: firstTestEndpoint.Key(),
|
||||||
ResponseTime: 1500,
|
ResponseTime: 1500,
|
||||||
ExpectedColor: badgeColorHexVeryBad,
|
ExpectedColor: badgeColorHexVeryBad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: secondTestEndpoint.Key(),
|
||||||
|
ResponseTime: 50,
|
||||||
|
ExpectedColor: badgeColorHexAwesome,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: secondTestEndpoint.Key(),
|
||||||
|
ResponseTime: 1500,
|
||||||
|
ExpectedColor: badgeColorHexPassable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: secondTestEndpoint.Key(),
|
||||||
|
ResponseTime: 2222,
|
||||||
|
ExpectedColor: badgeColorHexBad,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run("response-time-"+strconv.Itoa(scenario.ResponseTime), func(t *testing.T) {
|
t.Run(scenario.Key+"-response-time-"+strconv.Itoa(scenario.ResponseTime), func(t *testing.T) {
|
||||||
if getBadgeColorFromResponseTime(scenario.ResponseTime) != scenario.ExpectedColor {
|
if getBadgeColorFromResponseTime(scenario.ResponseTime, scenario.Key, cfg) != scenario.ExpectedColor {
|
||||||
t.Errorf("expected %s from %d, got %v", scenario.ExpectedColor, scenario.ResponseTime, getBadgeColorFromResponseTime(scenario.ResponseTime))
|
t.Errorf("expected %s from %d, got %v", scenario.ExpectedColor, scenario.ResponseTime, getBadgeColorFromResponseTime(scenario.ResponseTime, scenario.Key, cfg))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/storage/store"
|
||||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/storage/store"
|
||||||
"github.com/TwiN/gatus/v3/watchdog"
|
"github.com/TwiN/gatus/v4/watchdog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResponseTimeChart(t *testing.T) {
|
func TestResponseTimeChart(t *testing.T) {
|
||||||
@@ -30,7 +30,7 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &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[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
router := CreateRouter("../../web/static", cfg.Security, nil, cfg.Metrics)
|
router := CreateRouter(cfg)
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
@@ -58,11 +58,6 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
Path: "/api/v1/endpoints/invalid_key/response-times/7d/chart.svg",
|
Path: "/api/v1/endpoints/invalid_key/response-times/7d/chart.svg",
|
||||||
ExpectedCode: http.StatusNotFound,
|
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 {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"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.
|
// ConfigHandler is a handler that returns information for the front end of the application.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/security"
|
"github.com/TwiN/gatus/v4/security"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,20 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/storage/store/common/paging"
|
"github.com/TwiN/gatus/v4/config/remote"
|
||||||
"github.com/TwiN/gocache"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
|
"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"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,48 +33,89 @@ var (
|
|||||||
// EndpointStatuses handles requests to retrieve all EndpointStatus
|
// EndpointStatuses handles requests to retrieve all EndpointStatus
|
||||||
// Due to the size of the response, this function leverages a cache.
|
// Due to the size of the response, this function leverages a cache.
|
||||||
// Must not be wrapped by GzipHandler
|
// Must not be wrapped by GzipHandler
|
||||||
func EndpointStatuses(writer http.ResponseWriter, r *http.Request) {
|
func EndpointStatuses(cfg *config.Config) http.HandlerFunc {
|
||||||
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
return func(writer http.ResponseWriter, r *http.Request) {
|
||||||
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
||||||
var exists bool
|
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||||
var value interface{}
|
var exists bool
|
||||||
if gzipped {
|
var value interface{}
|
||||||
writer.Header().Set("Content-Encoding", "gzip")
|
|
||||||
value, exists = cache.Get(fmt.Sprintf("endpoint-status-%d-%d-gzipped", page, pageSize))
|
|
||||||
} else {
|
|
||||||
value, exists = cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
if !exists {
|
|
||||||
var err error
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
gzipWriter := gzip.NewWriter(buffer)
|
|
||||||
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[handler][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
|
|
||||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err = json.Marshal(endpointStatuses)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[handler][EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
|
|
||||||
http.Error(writer, "unable to marshal object to JSON", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = gzipWriter.Write(data)
|
|
||||||
_ = gzipWriter.Close()
|
|
||||||
gzippedData := buffer.Bytes()
|
|
||||||
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
|
|
||||||
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d-gzipped", page, pageSize), gzippedData, cacheTTL)
|
|
||||||
if gzipped {
|
if gzipped {
|
||||||
data = gzippedData
|
writer.Header().Set("Content-Encoding", "gzip")
|
||||||
|
value, exists = cache.Get(fmt.Sprintf("endpoint-status-%d-%d-gzipped", page, pageSize))
|
||||||
|
} else {
|
||||||
|
value, exists = cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
|
||||||
}
|
}
|
||||||
} else {
|
var data []byte
|
||||||
data = value.([]byte)
|
if !exists {
|
||||||
|
var err error
|
||||||
|
buffer := &bytes.Buffer{}
|
||||||
|
gzipWriter := gzip.NewWriter(buffer)
|
||||||
|
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[handler][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
|
||||||
|
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ALPHA: Retrieve endpoint statuses from remote instances
|
||||||
|
if endpointStatusesFromRemote, err := getEndpointStatusesFromRemoteInstances(cfg.Remote); err != nil {
|
||||||
|
log.Printf("[handler][EndpointStatuses] Silently failed to retrieve endpoint statuses from remote: %s", err.Error())
|
||||||
|
} else if endpointStatusesFromRemote != nil {
|
||||||
|
endpointStatuses = append(endpointStatuses, endpointStatusesFromRemote...)
|
||||||
|
}
|
||||||
|
// Marshal endpoint statuses to JSON
|
||||||
|
data, err = json.Marshal(endpointStatuses)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[handler][EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
|
||||||
|
http.Error(writer, "unable to marshal object to JSON", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = gzipWriter.Write(data)
|
||||||
|
_ = gzipWriter.Close()
|
||||||
|
gzippedData := buffer.Bytes()
|
||||||
|
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
|
||||||
|
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d-gzipped", page, pageSize), gzippedData, cacheTTL)
|
||||||
|
if gzipped {
|
||||||
|
data = gzippedData
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = value.([]byte)
|
||||||
|
}
|
||||||
|
writer.Header().Add("Content-Type", "application/json")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = writer.Write(data)
|
||||||
}
|
}
|
||||||
writer.Header().Add("Content-Type", "application/json")
|
}
|
||||||
writer.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = writer.Write(data)
|
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*core.EndpointStatus, error) {
|
||||||
|
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var endpointStatusesFromAllRemotes []*core.EndpointStatus
|
||||||
|
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
||||||
|
for _, instance := range remoteConfig.Instances {
|
||||||
|
response, err := httpClient.Get(instance.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
_ = response.Body.Close()
|
||||||
|
log.Printf("[handler][getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var endpointStatuses []*core.EndpointStatus
|
||||||
|
if err = json.Unmarshal(body, &endpointStatuses); err != nil {
|
||||||
|
_ = response.Body.Close()
|
||||||
|
log.Printf("[handler][getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = response.Body.Close()
|
||||||
|
for _, endpointStatus := range endpointStatuses {
|
||||||
|
endpointStatus.Name = instance.EndpointPrefix + endpointStatus.Name
|
||||||
|
}
|
||||||
|
endpointStatusesFromAllRemotes = append(endpointStatusesFromAllRemotes, endpointStatuses...)
|
||||||
|
}
|
||||||
|
return endpointStatusesFromAllRemotes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name
|
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name
|
||||||
|
|||||||
@@ -6,17 +6,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/storage/store"
|
||||||
"github.com/TwiN/gatus/v3/watchdog"
|
"github.com/TwiN/gatus/v4/watchdog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
firstCondition = core.Condition("[STATUS] == 200")
|
|
||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
|
||||||
|
|
||||||
timestamp = time.Now()
|
timestamp = time.Now()
|
||||||
|
|
||||||
testEndpoint = core.Endpoint{
|
testEndpoint = core.Endpoint{
|
||||||
@@ -26,7 +22,7 @@ var (
|
|||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
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,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
@@ -101,7 +97,7 @@ func TestEndpointStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &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[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
router := CreateRouter("../../web/static", cfg.Security, nil, cfg.Metrics)
|
router := CreateRouter(cfg)
|
||||||
|
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -131,11 +127,6 @@ func TestEndpointStatus(t *testing.T) {
|
|||||||
Path: "/api/v1/endpoints/invalid_key/statuses",
|
Path: "/api/v1/endpoints/invalid_key/statuses",
|
||||||
ExpectedCode: http.StatusNotFound,
|
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 {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
@@ -162,7 +153,7 @@ func TestEndpointStatuses(t *testing.T) {
|
|||||||
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||||
firstResult.Timestamp = time.Time{}
|
firstResult.Timestamp = time.Time{}
|
||||||
secondResult.Timestamp = time.Time{}
|
secondResult.Timestamp = time.Time{}
|
||||||
router := CreateRouter("../../web/static", nil, nil, false)
|
router := CreateRouter(&config.Config{Metrics: true})
|
||||||
|
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -201,12 +192,6 @@ func TestEndpointStatuses(t *testing.T) {
|
|||||||
ExpectedCode: http.StatusOK,
|
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"}]}]`,
|
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 {
|
for _, scenario := range scenarios {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FavIcon handles requests for /favicon.ico
|
|
||||||
func FavIcon(staticFolder string) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFavIcon(t *testing.T) {
|
|
||||||
router := CreateRouter("../../web/static", nil, nil, false)
|
|
||||||
type Scenario struct {
|
|
||||||
Name string
|
|
||||||
Path string
|
|
||||||
ExpectedCode int
|
|
||||||
}
|
|
||||||
scenarios := []Scenario{
|
|
||||||
{
|
|
||||||
Name: "favicon",
|
|
||||||
Path: "/favicon.ico",
|
|
||||||
ExpectedCode: http.StatusOK,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, scenario := range scenarios {
|
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
|
||||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
|
||||||
responseRecorder := httptest.NewRecorder()
|
|
||||||
router.ServeHTTP(responseRecorder, request)
|
|
||||||
if responseRecorder.Code != scenario.ExpectedCode {
|
|
||||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +1,50 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config/ui"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/security"
|
"github.com/TwiN/gatus/v4/web"
|
||||||
"github.com/TwiN/health"
|
"github.com/TwiN/health"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateRouter(staticFolder string, securityConfig *security.Config, uiConfig *ui.Config, enabledMetrics bool) *mux.Router {
|
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
if enabledMetrics {
|
if cfg.Metrics {
|
||||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||||
}
|
}
|
||||||
api := router.PathPrefix("/api").Subrouter()
|
api := router.PathPrefix("/api").Subrouter()
|
||||||
protected := api.PathPrefix("/").Subrouter()
|
protected := api.PathPrefix("/").Subrouter()
|
||||||
unprotected := api.PathPrefix("/").Subrouter()
|
unprotected := api.PathPrefix("/").Subrouter()
|
||||||
if securityConfig != nil {
|
if cfg.Security != nil {
|
||||||
if err := securityConfig.RegisterHandlers(router); err != nil {
|
if err := cfg.Security.RegisterHandlers(router); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := securityConfig.ApplySecurityMiddleware(protected); err != nil {
|
if err := cfg.Security.ApplySecurityMiddleware(protected); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Endpoints
|
// Endpoints
|
||||||
unprotected.Handle("/v1/config", ConfigHandler{securityConfig: securityConfig}).Methods("GET")
|
unprotected.Handle("/v1/config", ConfigHandler{securityConfig: cfg.Security}).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/statuses", EndpointStatuses(cfg)).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")
|
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}/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}/badge.svg", ResponseTimeBadge(cfg)).Methods("GET")
|
||||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).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
|
// Misc
|
||||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||||
router.HandleFunc("/favicon.ico", FavIcon(staticFolder)).Methods("GET")
|
|
||||||
// SPA
|
// SPA
|
||||||
router.HandleFunc("/services/{name}", SinglePageApplication(staticFolder, uiConfig)).Methods("GET") // XXX: Remove this in v4.0.0
|
router.HandleFunc("/endpoints/{name}", SinglePageApplication(cfg.UI)).Methods("GET")
|
||||||
router.HandleFunc("/endpoints/{name}", SinglePageApplication(staticFolder, uiConfig)).Methods("GET")
|
router.HandleFunc("/", SinglePageApplication(cfg.UI)).Methods("GET")
|
||||||
router.HandleFunc("/", SinglePageApplication(staticFolder, uiConfig)).Methods("GET")
|
|
||||||
// Everything else falls back on static content
|
// Everything else falls back on static content
|
||||||
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder))))
|
staticFileSystem, err := fs.Sub(static.FileSystem, static.RootPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.FS(staticFileSystem))))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v4/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateRouter(t *testing.T) {
|
func TestCreateRouter(t *testing.T) {
|
||||||
router := CreateRouter("../../web/static", nil, nil, true)
|
router := CreateRouter(&config.Config{Metrics: true})
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
@@ -26,16 +28,32 @@ func TestCreateRouter(t *testing.T) {
|
|||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "scripts",
|
Name: "favicon.ico",
|
||||||
|
Path: "/favicon.ico",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "app.js",
|
||||||
Path: "/js/app.js",
|
Path: "/js/app.js",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "scripts-gzipped",
|
Name: "app.js-gzipped",
|
||||||
Path: "/js/app.js",
|
Path: "/js/app.js",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
Gzip: true,
|
Gzip: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "chunk-vendors.js",
|
||||||
|
Path: "/js/chunk-vendors.js",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chunk-vendors.js-gzipped",
|
||||||
|
Path: "/js/chunk-vendors.js",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
Gzip: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "index-redirect",
|
Name: "index-redirect",
|
||||||
Path: "/index.html",
|
Path: "/index.html",
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "embed"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config/ui"
|
"github.com/TwiN/gatus/v4/config/ui"
|
||||||
|
"github.com/TwiN/gatus/v4/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SinglePageApplication(staticFolder string, ui *ui.Config) http.HandlerFunc {
|
func SinglePageApplication(ui *ui.Config) http.HandlerFunc {
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
t, err := template.ParseFiles(staticFolder + "/index.html")
|
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[handler][SinglePageApplication] Failed to parse template:", err.Error())
|
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
log.Println("[handler][SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||||
|
http.Error(writer, "Failed to parse template. This should never happen, because the template is validated on start.", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", "text/html")
|
writer.Header().Set("Content-Type", "text/html")
|
||||||
err = t.Execute(writer, ui)
|
err = t.Execute(writer, ui)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[handler][SinglePageApplication] Failed to parse template:", err.Error())
|
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
log.Println("[handler][SinglePageApplication] Failed to execute template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||||
|
http.Error(writer, "Failed to execute template. This should never happen, because the template is validated on start.", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/core"
|
"github.com/TwiN/gatus/v4/core"
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/storage/store"
|
||||||
"github.com/TwiN/gatus/v3/watchdog"
|
"github.com/TwiN/gatus/v4/watchdog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSinglePageApplication(t *testing.T) {
|
func TestSinglePageApplication(t *testing.T) {
|
||||||
@@ -30,7 +30,7 @@ func TestSinglePageApplication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
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[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
router := CreateRouter("../../web/static", cfg.Security, nil, cfg.Metrics)
|
router := CreateRouter(cfg)
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
@@ -48,11 +48,6 @@ func TestSinglePageApplication(t *testing.T) {
|
|||||||
Path: "/endpoints/core_frontend",
|
Path: "/endpoints/core_frontend",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
{ // XXX: Remove this in v4.0.0
|
|
||||||
Name: "frontend-service",
|
|
||||||
Path: "/services/core_frontend",
|
|
||||||
ExpectedCode: http.StatusOK,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/storage/store/common"
|
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+20
-3
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/jsonpath"
|
"github.com/TwiN/gatus/v4/jsonpath"
|
||||||
"github.com/TwiN/gatus/v3/pattern"
|
"github.com/TwiN/gatus/v4/pattern"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,6 +46,9 @@ const (
|
|||||||
// Values that could replace the placeholder: 4461677039 (~52 days)
|
// Values that could replace the placeholder: 4461677039 (~52 days)
|
||||||
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
|
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
|
||||||
|
|
||||||
|
// DomainExpirationPlaceholder is a placeholder for the duration before the domain expires, in milliseconds.
|
||||||
|
DomainExpirationPlaceholder = "[DOMAIN_EXPIRATION]"
|
||||||
|
|
||||||
// LengthFunctionPrefix is the prefix for the length function
|
// LengthFunctionPrefix is the prefix for the length function
|
||||||
//
|
//
|
||||||
// Usage: len([BODY].articles) == 10, len([BODY].name) > 5
|
// Usage: len([BODY].articles) == 10, len([BODY].name) > 5
|
||||||
@@ -142,9 +145,21 @@ func (c Condition) hasBodyPlaceholder() bool {
|
|||||||
return strings.Contains(string(c), BodyPlaceholder)
|
return strings.Contains(string(c), BodyPlaceholder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasDomainExpirationPlaceholder checks whether the condition has a DomainExpirationPlaceholder
|
||||||
|
// Used for determining whether a whois operation is necessary
|
||||||
|
func (c Condition) hasDomainExpirationPlaceholder() bool {
|
||||||
|
return strings.Contains(string(c), DomainExpirationPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasIPPlaceholder checks whether the condition has an IPPlaceholder
|
||||||
|
// Used for determining whether an IP lookup is necessary
|
||||||
|
func (c Condition) hasIPPlaceholder() bool {
|
||||||
|
return strings.Contains(string(c), IPPlaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
// isEqual compares two strings.
|
// isEqual compares two strings.
|
||||||
//
|
//
|
||||||
// Supports the pattern and the any functions.
|
// Supports the "pat" and the "any" functions.
|
||||||
// i.e. if one of the parameters starts with PatternFunctionPrefix and ends with FunctionSuffix, it will be treated like
|
// i.e. if one of the parameters starts with PatternFunctionPrefix and ends with FunctionSuffix, it will be treated like
|
||||||
// a pattern.
|
// a pattern.
|
||||||
func isEqual(first, second string) bool {
|
func isEqual(first, second string) bool {
|
||||||
@@ -219,6 +234,8 @@ func sanitizeAndResolve(elements []string, result *Result) ([]string, []string)
|
|||||||
element = strconv.FormatBool(result.Connected)
|
element = strconv.FormatBool(result.Connected)
|
||||||
case CertificateExpirationPlaceholder:
|
case CertificateExpirationPlaceholder:
|
||||||
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
|
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
|
||||||
|
case DomainExpirationPlaceholder:
|
||||||
|
element = strconv.FormatInt(result.DomainExpiration.Milliseconds(), 10)
|
||||||
default:
|
default:
|
||||||
// if contains the BodyPlaceholder, then evaluate json path
|
// if contains the BodyPlaceholder, then evaluate json path
|
||||||
if strings.Contains(element, BodyPlaceholder) {
|
if strings.Contains(element, BodyPlaceholder) {
|
||||||
|
|||||||
+13
-13
@@ -2,8 +2,9 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/pattern"
|
"github.com/TwiN/gatus/v4/pattern"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIntegrationQuery(t *testing.T) {
|
func TestIntegrationQuery(t *testing.T) {
|
||||||
@@ -21,7 +22,7 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
inputURL: "8.8.8.8",
|
inputURL: "1.1.1.1",
|
||||||
expectedDNSCode: "NOERROR",
|
expectedDNSCode: "NOERROR",
|
||||||
expectedBody: "93.184.216.34",
|
expectedBody: "93.184.216.34",
|
||||||
},
|
},
|
||||||
@@ -31,7 +32,7 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
QueryType: "AAAA",
|
QueryType: "AAAA",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
inputURL: "8.8.8.8",
|
inputURL: "1.1.1.1",
|
||||||
expectedDNSCode: "NOERROR",
|
expectedDNSCode: "NOERROR",
|
||||||
expectedBody: "2606:2800:220:1:248:1893:25c8:1946",
|
expectedBody: "2606:2800:220:1:248:1893:25c8:1946",
|
||||||
},
|
},
|
||||||
@@ -39,11 +40,11 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
name: "test DNS with type CNAME",
|
name: "test DNS with type CNAME",
|
||||||
inputDNS: DNS{
|
inputDNS: DNS{
|
||||||
QueryType: "CNAME",
|
QueryType: "CNAME",
|
||||||
QueryName: "doc.google.com.",
|
QueryName: "en.wikipedia.org.",
|
||||||
},
|
},
|
||||||
inputURL: "8.8.8.8",
|
inputURL: "1.1.1.1",
|
||||||
expectedDNSCode: "NOERROR",
|
expectedDNSCode: "NOERROR",
|
||||||
expectedBody: "writely.l.google.com.",
|
expectedBody: "dyna.wikimedia.org.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test DNS with type MX",
|
name: "test DNS with type MX",
|
||||||
@@ -51,7 +52,7 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
QueryType: "MX",
|
QueryType: "MX",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
inputURL: "8.8.8.8",
|
inputURL: "1.1.1.1",
|
||||||
expectedDNSCode: "NOERROR",
|
expectedDNSCode: "NOERROR",
|
||||||
expectedBody: ".",
|
expectedBody: ".",
|
||||||
},
|
},
|
||||||
@@ -61,7 +62,7 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
QueryType: "NS",
|
QueryType: "NS",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
inputURL: "8.8.8.8",
|
inputURL: "1.1.1.1",
|
||||||
expectedDNSCode: "NOERROR",
|
expectedDNSCode: "NOERROR",
|
||||||
expectedBody: "*.iana-servers.net.",
|
expectedBody: "*.iana-servers.net.",
|
||||||
},
|
},
|
||||||
@@ -69,15 +70,14 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
name: "test DNS with fake type and retrieve error",
|
name: "test DNS with fake type and retrieve error",
|
||||||
inputDNS: DNS{
|
inputDNS: DNS{
|
||||||
QueryType: "B",
|
QueryType: "B",
|
||||||
QueryName: "google",
|
QueryName: "example",
|
||||||
},
|
},
|
||||||
inputURL: "8.8.8.8",
|
inputURL: "1.1.1.1",
|
||||||
isErrExpected: true,
|
isErrExpected: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
dns := test.inputDNS
|
dns := test.inputDNS
|
||||||
result := &Result{}
|
result := &Result{}
|
||||||
@@ -86,9 +86,8 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
t.Errorf("there should be errors")
|
t.Errorf("there should be errors")
|
||||||
}
|
}
|
||||||
if result.DNSRCode != test.expectedDNSCode {
|
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" {
|
if test.inputDNS.QueryType == "NS" {
|
||||||
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
// 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)) {
|
if !pattern.Match(test.expectedBody, string(result.body)) {
|
||||||
@@ -100,6 +99,7 @@ func TestIntegrationQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+129
-32
@@ -12,12 +12,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
"github.com/TwiN/gatus/v3/core/ui"
|
"github.com/TwiN/gatus/v4/core/ui"
|
||||||
"github.com/TwiN/gatus/v3/util"
|
"github.com/TwiN/gatus/v4/util"
|
||||||
|
"github.com/TwiN/whois"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EndpointType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HostHeader is the name of the header used to specify the host
|
// HostHeader is the name of the header used to specify the host
|
||||||
HostHeader = "Host"
|
HostHeader = "Host"
|
||||||
@@ -30,6 +33,14 @@ const (
|
|||||||
|
|
||||||
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
||||||
GatusUserAgent = "Gatus/1.0"
|
GatusUserAgent = "Gatus/1.0"
|
||||||
|
|
||||||
|
EndpointTypeDNS EndpointType = "DNS"
|
||||||
|
EndpointTypeTCP EndpointType = "TCP"
|
||||||
|
EndpointTypeICMP EndpointType = "ICMP"
|
||||||
|
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
||||||
|
EndpointTypeTLS EndpointType = "TLS"
|
||||||
|
EndpointTypeHTTP EndpointType = "HTTP"
|
||||||
|
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -44,6 +55,15 @@ var (
|
|||||||
|
|
||||||
// ErrEndpointWithInvalidNameOrGroup is the error with which Gatus will panic if an endpoint has an invalid character where it shouldn't
|
// ErrEndpointWithInvalidNameOrGroup is the error with which Gatus will panic if an endpoint has an invalid character where it shouldn't
|
||||||
ErrEndpointWithInvalidNameOrGroup = errors.New("endpoint name and group must not have \" or \\")
|
ErrEndpointWithInvalidNameOrGroup = errors.New("endpoint name and group must not have \" or \\")
|
||||||
|
|
||||||
|
// ErrUnknownEndpointType is the error with which Gatus will panic if an endpoint has an unknown type
|
||||||
|
ErrUnknownEndpointType = errors.New("unknown endpoint type")
|
||||||
|
|
||||||
|
// ErrInvalidEndpointIntervalForDomainExpirationPlaceholder is the error with which Gatus will panic if an endpoint
|
||||||
|
// has both an interval smaller than 5 minutes and a condition with DomainExpirationPlaceholder.
|
||||||
|
// This is because the free whois service we are using should not be abused, especially considering the fact that
|
||||||
|
// the data takes a while to be updated.
|
||||||
|
ErrInvalidEndpointIntervalForDomainExpirationPlaceholder = errors.New("the minimum interval for an endpoint with a condition using the " + DomainExpirationPlaceholder + " placeholder is 300s (5m)")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Endpoint is the configuration of a monitored
|
// Endpoint is the configuration of a monitored
|
||||||
@@ -79,7 +99,7 @@ type Endpoint struct {
|
|||||||
Interval time.Duration `yaml:"interval,omitempty"`
|
Interval time.Duration `yaml:"interval,omitempty"`
|
||||||
|
|
||||||
// Conditions used to determine the health of the endpoint
|
// 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 is the alerting configuration for the endpoint in case of failure
|
||||||
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
||||||
@@ -105,16 +125,42 @@ func (endpoint Endpoint) IsEnabled() bool {
|
|||||||
return *endpoint.Enabled
|
return *endpoint.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of fields that have one
|
// 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
|
||||||
|
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
||||||
|
return EndpointTypeHTTP
|
||||||
|
default:
|
||||||
|
return EndpointTypeUNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
||||||
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
||||||
// Set default values
|
// Set default values
|
||||||
if endpoint.ClientConfig == nil {
|
if endpoint.ClientConfig == nil {
|
||||||
endpoint.ClientConfig = client.GetDefaultConfig()
|
endpoint.ClientConfig = client.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
endpoint.ClientConfig.ValidateAndSetDefaults()
|
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig == nil {
|
if endpoint.UIConfig == nil {
|
||||||
endpoint.UIConfig = ui.GetDefaultConfig()
|
endpoint.UIConfig = ui.GetDefaultConfig()
|
||||||
|
} else {
|
||||||
|
if err := endpoint.UIConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if endpoint.Interval == 0 {
|
if endpoint.Interval == 0 {
|
||||||
endpoint.Interval = 1 * time.Minute
|
endpoint.Interval = 1 * time.Minute
|
||||||
@@ -151,9 +197,19 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
|||||||
if len(endpoint.Conditions) == 0 {
|
if len(endpoint.Conditions) == 0 {
|
||||||
return ErrEndpointWithNoCondition
|
return ErrEndpointWithNoCondition
|
||||||
}
|
}
|
||||||
|
if endpoint.Interval < 5*time.Minute {
|
||||||
|
for _, condition := range endpoint.Conditions {
|
||||||
|
if condition.hasDomainExpirationPlaceholder() {
|
||||||
|
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if endpoint.DNS != nil {
|
if endpoint.DNS != nil {
|
||||||
return endpoint.DNS.validateAndSetDefault()
|
return endpoint.DNS.validateAndSetDefault()
|
||||||
}
|
}
|
||||||
|
if endpoint.Type() == EndpointTypeUNKNOWN {
|
||||||
|
return ErrUnknownEndpointType
|
||||||
|
}
|
||||||
// Make sure that the request can be created
|
// Make sure that the request can be created
|
||||||
_, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body)))
|
_, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -178,7 +234,26 @@ func (endpoint Endpoint) Key() string {
|
|||||||
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
||||||
func (endpoint *Endpoint) EvaluateHealth() *Result {
|
func (endpoint *Endpoint) EvaluateHealth() *Result {
|
||||||
result := &Result{Success: true, Errors: []string{}}
|
result := &Result{Success: true, Errors: []string{}}
|
||||||
endpoint.getIP(result)
|
// Parse or extract hostname from URL
|
||||||
|
if endpoint.DNS != nil {
|
||||||
|
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53")
|
||||||
|
} else {
|
||||||
|
urlObject, err := url.Parse(endpoint.URL)
|
||||||
|
if err != nil {
|
||||||
|
result.AddError(err.Error())
|
||||||
|
} else {
|
||||||
|
result.Hostname = urlObject.Hostname()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Retrieve IP if necessary
|
||||||
|
if endpoint.needsToRetrieveIP() {
|
||||||
|
endpoint.getIP(result)
|
||||||
|
}
|
||||||
|
// Retrieve domain expiration if necessary
|
||||||
|
if endpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
||||||
|
endpoint.getDomainExpiration(result)
|
||||||
|
}
|
||||||
|
//
|
||||||
if len(result.Errors) == 0 {
|
if len(result.Errors) == 0 {
|
||||||
endpoint.call(result)
|
endpoint.call(result)
|
||||||
} else {
|
} else {
|
||||||
@@ -194,23 +269,21 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
|||||||
// No need to keep the body after the endpoint has been evaluated
|
// No need to keep the body after the endpoint has been evaluated
|
||||||
result.body = nil
|
result.body = nil
|
||||||
// Clean up parameters that we don't need to keep in the results
|
// Clean up parameters that we don't need to keep in the results
|
||||||
|
if endpoint.UIConfig.HideURL {
|
||||||
|
for errIdx, errorString := range result.Errors {
|
||||||
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, endpoint.URL, "<redacted>")
|
||||||
|
}
|
||||||
|
}
|
||||||
if endpoint.UIConfig.HideHostname {
|
if endpoint.UIConfig.HideHostname {
|
||||||
|
for errIdx, errorString := range result.Errors {
|
||||||
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
||||||
|
}
|
||||||
result.Hostname = ""
|
result.Hostname = ""
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) getIP(result *Result) {
|
func (endpoint *Endpoint) getIP(result *Result) {
|
||||||
if endpoint.DNS != nil {
|
|
||||||
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53")
|
|
||||||
} else {
|
|
||||||
urlObject, err := url.Parse(endpoint.URL)
|
|
||||||
if err != nil {
|
|
||||||
result.AddError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.Hostname = urlObject.Hostname()
|
|
||||||
}
|
|
||||||
ips, err := net.LookupIP(result.Hostname)
|
ips, err := net.LookupIP(result.Hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@@ -219,26 +292,30 @@ func (endpoint *Endpoint) getIP(result *Result) {
|
|||||||
result.IP = ips[0].String()
|
result.IP = ips[0].String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (endpoint *Endpoint) getDomainExpiration(result *Result) {
|
||||||
|
whoisClient := whois.NewClient()
|
||||||
|
if whoisResponse, err := whoisClient.QueryAndParse(result.Hostname); err != nil {
|
||||||
|
result.AddError("error querying and parsing hostname using whois client: " + err.Error())
|
||||||
|
} else {
|
||||||
|
result.DomainExpiration = time.Until(whoisResponse.ExpirationDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) call(result *Result) {
|
func (endpoint *Endpoint) call(result *Result) {
|
||||||
var request *http.Request
|
var request *http.Request
|
||||||
var response *http.Response
|
var response *http.Response
|
||||||
var err error
|
var err error
|
||||||
var certificate *x509.Certificate
|
var certificate *x509.Certificate
|
||||||
isTypeDNS := endpoint.DNS != nil
|
endpointType := endpoint.Type()
|
||||||
isTypeTCP := strings.HasPrefix(endpoint.URL, "tcp://")
|
if endpointType == EndpointTypeHTTP {
|
||||||
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 {
|
|
||||||
request = endpoint.buildHTTPRequest()
|
request = endpoint.buildHTTPRequest()
|
||||||
}
|
}
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
if isTypeDNS {
|
if endpointType == EndpointTypeDNS {
|
||||||
endpoint.DNS.query(endpoint.URL, result)
|
endpoint.DNS.query(endpoint.URL, result)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if isTypeSTARTTLS || isTypeTLS {
|
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS {
|
||||||
if isTypeSTARTTLS {
|
if endpointType == EndpointTypeSTARTTLS {
|
||||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig)
|
||||||
@@ -249,10 +326,10 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||||
} else if isTypeTCP {
|
} else if endpointType == EndpointTypeTCP {
|
||||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
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)
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
||||||
@@ -272,7 +349,7 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
if endpoint.needsToReadBody() {
|
if endpoint.needsToReadBody() {
|
||||||
result.body, err = io.ReadAll(response.Body)
|
result.body, err = io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError("error reading response body:" + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,7 +376,7 @@ func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsToReadBody checks if there's any conditions that requires the response body to be read
|
// needsToReadBody checks if there's any condition that requires the response body to be read
|
||||||
func (endpoint *Endpoint) needsToReadBody() bool {
|
func (endpoint *Endpoint) needsToReadBody() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range endpoint.Conditions {
|
||||||
if condition.hasBodyPlaceholder() {
|
if condition.hasBodyPlaceholder() {
|
||||||
@@ -308,3 +385,23 @@ func (endpoint *Endpoint) needsToReadBody() bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
||||||
|
func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||||
|
for _, condition := range endpoint.Conditions {
|
||||||
|
if condition.hasDomainExpirationPlaceholder() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
||||||
|
func (endpoint *Endpoint) needsToRetrieveIP() bool {
|
||||||
|
for _, condition := range endpoint.Conditions {
|
||||||
|
if condition.hasIPPlaceholder() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import "github.com/TwiN/gatus/v3/util"
|
import "github.com/TwiN/gatus/v4/util"
|
||||||
|
|
||||||
// EndpointStatus contains the evaluation Results of an Endpoint
|
// EndpointStatus contains the evaluation Results of an Endpoint
|
||||||
type EndpointStatus struct {
|
type EndpointStatus struct {
|
||||||
|
|||||||
+453
-82
@@ -1,15 +1,245 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
"github.com/TwiN/gatus/v4/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v3/client"
|
"github.com/TwiN/gatus/v4/client"
|
||||||
|
"github.com/TwiN/gatus/v4/core/ui"
|
||||||
|
"github.com/TwiN/gatus/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestEndpoint(t *testing.T) {
|
||||||
|
defer client.InjectHTTPClient(nil)
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Endpoint Endpoint
|
||||||
|
ExpectedResult *Result
|
||||||
|
MockRoundTripper test.MockRoundTripper
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "success",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[STATUS] == 200", "[BODY].status == UP", "[CERTIFICATE_EXPIRATION] > 24h"},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: true,
|
||||||
|
Connected: true,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
{Condition: "[STATUS] == 200", Success: true},
|
||||||
|
{Condition: "[BODY].status == UP", Success: true},
|
||||||
|
{Condition: "[CERTIFICATE_EXPIRATION] > 24h", Success: true},
|
||||||
|
},
|
||||||
|
DomainExpiration: 0, // Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
},
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString(`{"status": "UP"}`)),
|
||||||
|
TLS: &tls.ConnectionState{PeerCertificates: []*x509.Certificate{{NotAfter: time.Now().Add(9999 * time.Hour)}}},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "failed-body-condition",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: false,
|
||||||
|
Connected: true,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
{Condition: "[STATUS] == 200", Success: true},
|
||||||
|
{Condition: "[BODY].status (DOWN) == UP", Success: false},
|
||||||
|
},
|
||||||
|
DomainExpiration: 0, // Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
},
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`{"status": "DOWN"}`))}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "failed-status-condition",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[STATUS] == 200"},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: false,
|
||||||
|
Connected: true,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
{Condition: "[STATUS] (502) == 200", Success: false},
|
||||||
|
},
|
||||||
|
DomainExpiration: 0, // Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
},
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusBadGateway, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "condition-with-failed-certificate-expiration",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[CERTIFICATE_EXPIRATION] > 100h"},
|
||||||
|
UIConfig: &ui.Config{DontResolveFailedConditions: true},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: false,
|
||||||
|
Connected: true,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
// Because UIConfig.DontResolveFailedConditions is true, the values in the condition should not be resolved
|
||||||
|
{Condition: "[CERTIFICATE_EXPIRATION] > 100h", Success: false},
|
||||||
|
},
|
||||||
|
DomainExpiration: 0, // Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
},
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: http.NoBody,
|
||||||
|
TLS: &tls.ConnectionState{PeerCertificates: []*x509.Certificate{{NotAfter: time.Now().Add(5 * time.Hour)}}},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "domain-expiration",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[DOMAIN_EXPIRATION] > 100h"},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: true,
|
||||||
|
Connected: true,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
{Condition: "[DOMAIN_EXPIRATION] > 100h", Success: true},
|
||||||
|
},
|
||||||
|
DomainExpiration: 999999 * time.Hour, // Note that this test only checks if it's non-zero.
|
||||||
|
},
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "endpoint-that-will-time-out-and-hidden-hostname",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "endpoint-that-will-time-out",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[CONNECTED] == true"},
|
||||||
|
UIConfig: &ui.Config{HideHostname: true},
|
||||||
|
ClientConfig: &client.Config{Timeout: time.Millisecond},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: false,
|
||||||
|
Connected: false,
|
||||||
|
Hostname: "", // Because Endpoint.UIConfig.HideHostname is true, this should be empty.
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] (false) == true", Success: false},
|
||||||
|
},
|
||||||
|
// Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
DomainExpiration: 0,
|
||||||
|
// Because Endpoint.UIConfig.HideHostname is true, the hostname should be replaced by <redacted>.
|
||||||
|
Errors: []string{`Get "https://<redacted>/health": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`},
|
||||||
|
},
|
||||||
|
MockRoundTripper: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "endpoint-that-will-time-out-and-hidden-url",
|
||||||
|
Endpoint: Endpoint{
|
||||||
|
Name: "endpoint-that-will-time-out",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{"[CONNECTED] == true"},
|
||||||
|
UIConfig: &ui.Config{HideURL: true},
|
||||||
|
ClientConfig: &client.Config{Timeout: time.Millisecond},
|
||||||
|
},
|
||||||
|
ExpectedResult: &Result{
|
||||||
|
Success: false,
|
||||||
|
Connected: false,
|
||||||
|
Hostname: "twin.sh",
|
||||||
|
ConditionResults: []*ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] (false) == true", Success: false},
|
||||||
|
},
|
||||||
|
// Because there's no [DOMAIN_EXPIRATION] condition, this is not resolved, so it should be 0.
|
||||||
|
DomainExpiration: 0,
|
||||||
|
// Because Endpoint.UIConfig.HideURL is true, the URL should be replaced by <redacted>.
|
||||||
|
Errors: []string{`Get "<redacted>": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`},
|
||||||
|
},
|
||||||
|
MockRoundTripper: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
if scenario.MockRoundTripper != nil {
|
||||||
|
mockClient := &http.Client{Transport: scenario.MockRoundTripper}
|
||||||
|
if scenario.Endpoint.ClientConfig != nil && scenario.Endpoint.ClientConfig.Timeout > 0 {
|
||||||
|
mockClient.Timeout = scenario.Endpoint.ClientConfig.Timeout
|
||||||
|
}
|
||||||
|
client.InjectHTTPClient(mockClient)
|
||||||
|
} else {
|
||||||
|
client.InjectHTTPClient(nil)
|
||||||
|
}
|
||||||
|
scenario.Endpoint.ValidateAndSetDefaults()
|
||||||
|
result := scenario.Endpoint.EvaluateHealth()
|
||||||
|
if result.Success != scenario.ExpectedResult.Success {
|
||||||
|
t.Errorf("Expected success to be %v, got %v", scenario.ExpectedResult.Success, result.Success)
|
||||||
|
}
|
||||||
|
if result.Connected != scenario.ExpectedResult.Connected {
|
||||||
|
t.Errorf("Expected connected to be %v, got %v", scenario.ExpectedResult.Connected, result.Connected)
|
||||||
|
}
|
||||||
|
if result.Hostname != scenario.ExpectedResult.Hostname {
|
||||||
|
t.Errorf("Expected hostname to be %v, got %v", scenario.ExpectedResult.Hostname, result.Hostname)
|
||||||
|
}
|
||||||
|
if len(result.ConditionResults) != len(scenario.ExpectedResult.ConditionResults) {
|
||||||
|
t.Errorf("Expected %v condition results, got %v", len(scenario.ExpectedResult.ConditionResults), len(result.ConditionResults))
|
||||||
|
} else {
|
||||||
|
for i, conditionResult := range result.ConditionResults {
|
||||||
|
if conditionResult.Condition != scenario.ExpectedResult.ConditionResults[i].Condition {
|
||||||
|
t.Errorf("Expected condition to be %v, got %v", scenario.ExpectedResult.ConditionResults[i].Condition, conditionResult.Condition)
|
||||||
|
}
|
||||||
|
if conditionResult.Success != scenario.ExpectedResult.ConditionResults[i].Success {
|
||||||
|
t.Errorf("Expected success of condition '%s' to be %v, got %v", conditionResult.Condition, scenario.ExpectedResult.ConditionResults[i].Success, conditionResult.Success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result.Errors) != len(scenario.ExpectedResult.Errors) {
|
||||||
|
t.Errorf("Expected %v errors, got %v", len(scenario.ExpectedResult.Errors), len(result.Errors))
|
||||||
|
} else {
|
||||||
|
for i, err := range result.Errors {
|
||||||
|
if err != scenario.ExpectedResult.Errors[i] {
|
||||||
|
t.Errorf("Expected error to be %v, got %v", scenario.ExpectedResult.Errors[i], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.DomainExpiration != scenario.ExpectedResult.DomainExpiration {
|
||||||
|
// Note that DomainExpiration is only resolved if there's a condition with the DomainExpirationPlaceholder in it.
|
||||||
|
// In other words, if there's no condition with [DOMAIN_EXPIRATION] in it, the DomainExpiration field will be 0.
|
||||||
|
// Because this is a live call, mocking it would be too much of a pain, so we're just going to check if
|
||||||
|
// the actual value is non-zero when the expected result is non-zero.
|
||||||
|
if scenario.ExpectedResult.DomainExpiration.Hours() > 0 && !(result.DomainExpiration.Hours() > 0) {
|
||||||
|
t.Errorf("Expected domain expiration to be non-zero, got %v", result.DomainExpiration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint_IsEnabled(t *testing.T) {
|
func TestEndpoint_IsEnabled(t *testing.T) {
|
||||||
if !(Endpoint{Enabled: nil}).IsEnabled() {
|
if !(Endpoint{Enabled: nil}).IsEnabled() {
|
||||||
t.Error("endpoint.IsEnabled() should've returned true, because Enabled was set to nil")
|
t.Error("endpoint.IsEnabled() should've returned true, because Enabled was set to nil")
|
||||||
@@ -22,12 +252,86 @@ func TestEndpoint_IsEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_Type(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
URL string
|
||||||
|
DNS *DNS
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
args args
|
||||||
|
want EndpointType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "1.1.1.1",
|
||||||
|
DNS: &DNS{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: EndpointTypeDNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "tcp://127.0.0.1:6379",
|
||||||
|
},
|
||||||
|
want: EndpointTypeTCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "icmp://example.com",
|
||||||
|
},
|
||||||
|
want: EndpointTypeICMP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "starttls://smtp.gmail.com:587",
|
||||||
|
},
|
||||||
|
want: EndpointTypeSTARTTLS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "tls://example.com:443",
|
||||||
|
},
|
||||||
|
want: EndpointTypeTLS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
},
|
||||||
|
want: EndpointTypeHTTP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "invalid://example.org",
|
||||||
|
},
|
||||||
|
want: EndpointTypeUNKNOWN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
URL: "no-scheme",
|
||||||
|
},
|
||||||
|
want: EndpointTypeUNKNOWN,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.want), func(t *testing.T) {
|
||||||
|
endpoint := Endpoint{
|
||||||
|
URL: tt.args.URL,
|
||||||
|
DNS: tt.args.DNS,
|
||||||
|
}
|
||||||
|
if got := endpoint.Type(); got != tt.want {
|
||||||
|
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaults(t *testing.T) {
|
func TestEndpoint_ValidateAndSetDefaults(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 200")
|
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []Condition{Condition("[STATUS] == 200")},
|
||||||
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
Alerts: []*alert.Alert{{Type: alert.TypePagerDuty}},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
endpoint.ValidateAndSetDefaults()
|
||||||
@@ -68,11 +372,10 @@ func TestEndpoint_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
|
func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 200")
|
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []Condition{Condition("[STATUS] == 200")},
|
||||||
ClientConfig: &client.Config{
|
ClientConfig: &client.Config{
|
||||||
Insecure: true,
|
Insecure: true,
|
||||||
IgnoreRedirect: true,
|
IgnoreRedirect: true,
|
||||||
@@ -95,73 +398,88 @@ func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithNoName(t *testing.T) {
|
|
||||||
defer func() { recover() }()
|
|
||||||
condition := Condition("[STATUS] == 200")
|
|
||||||
endpoint := &Endpoint{
|
|
||||||
Name: "",
|
|
||||||
URL: "http://example.com",
|
|
||||||
Conditions: []*Condition{&condition},
|
|
||||||
}
|
|
||||||
err := endpoint.ValidateAndSetDefaults()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Should've returned an error because endpoint didn't have a name, which is a mandatory field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithNoUrl(t *testing.T) {
|
|
||||||
defer func() { recover() }()
|
|
||||||
condition := Condition("[STATUS] == 200")
|
|
||||||
endpoint := &Endpoint{
|
|
||||||
Name: "example",
|
|
||||||
URL: "",
|
|
||||||
Conditions: []*Condition{&condition},
|
|
||||||
}
|
|
||||||
err := endpoint.ValidateAndSetDefaults()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Should've returned an error because endpoint didn't have an url, which is a mandatory field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithNoConditions(t *testing.T) {
|
|
||||||
defer func() { recover() }()
|
|
||||||
endpoint := &Endpoint{
|
|
||||||
Name: "example",
|
|
||||||
URL: "http://example.com",
|
|
||||||
Conditions: nil,
|
|
||||||
}
|
|
||||||
err := endpoint.ValidateAndSetDefaults()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Should've returned an error because endpoint didn't have at least 1 condition")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||||
conditionSuccess := Condition("[DNS_RCODE] == NOERROR")
|
|
||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "dns-test",
|
Name: "dns-test",
|
||||||
URL: "http://example.com",
|
URL: "https://example.com",
|
||||||
DNS: &DNS{
|
DNS: &DNS{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
Conditions: []*Condition{&conditionSuccess},
|
Conditions: []Condition{Condition("[DNS_RCODE] == NOERROR")},
|
||||||
}
|
}
|
||||||
err := endpoint.ValidateAndSetDefaults()
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
t.Error("did not expect an error, got", err)
|
||||||
}
|
}
|
||||||
if endpoint.DNS.QueryName != "example.com." {
|
if endpoint.DNS.QueryName != "example.com." {
|
||||||
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_ValidateAndSetDefaultsWithSimpleErrors(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
endpoint *Endpoint
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
endpoint: &Endpoint{
|
||||||
|
Name: "",
|
||||||
|
URL: "https://example.com",
|
||||||
|
Conditions: []Condition{Condition("[STATUS] == 200")},
|
||||||
|
},
|
||||||
|
expectedErr: ErrEndpointWithNoName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: &Endpoint{
|
||||||
|
Name: "endpoint-with-no-url",
|
||||||
|
URL: "",
|
||||||
|
Conditions: []Condition{Condition("[STATUS] == 200")},
|
||||||
|
},
|
||||||
|
expectedErr: ErrEndpointWithNoURL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: &Endpoint{
|
||||||
|
Name: "endpoint-with-no-conditions",
|
||||||
|
URL: "https://example.com",
|
||||||
|
Conditions: nil,
|
||||||
|
},
|
||||||
|
expectedErr: ErrEndpointWithNoCondition,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: &Endpoint{
|
||||||
|
Name: "domain-expiration-with-bad-interval",
|
||||||
|
URL: "https://example.com",
|
||||||
|
Interval: time.Minute,
|
||||||
|
Conditions: []Condition{Condition("[DOMAIN_EXPIRATION] > 720h")},
|
||||||
|
},
|
||||||
|
expectedErr: ErrInvalidEndpointIntervalForDomainExpirationPlaceholder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
endpoint: &Endpoint{
|
||||||
|
Name: "domain-expiration-with-good-interval",
|
||||||
|
URL: "https://example.com",
|
||||||
|
Interval: 5 * time.Minute,
|
||||||
|
Conditions: []Condition{Condition("[DOMAIN_EXPIRATION] > 720h")},
|
||||||
|
},
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.endpoint.Name, func(t *testing.T) {
|
||||||
|
if err := scenario.endpoint.ValidateAndSetDefaults(); err != scenario.expectedErr {
|
||||||
|
t.Errorf("Expected error %v, got %v", scenario.expectedErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint_buildHTTPRequest(t *testing.T) {
|
func TestEndpoint_buildHTTPRequest(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []Condition{condition},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
endpoint.ValidateAndSetDefaults()
|
||||||
request := endpoint.buildHTTPRequest()
|
request := endpoint.buildHTTPRequest()
|
||||||
@@ -181,7 +499,7 @@ func TestEndpoint_buildHTTPRequestWithCustomUserAgent(t *testing.T) {
|
|||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []Condition{condition},
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"User-Agent": "Test/2.0",
|
"User-Agent": "Test/2.0",
|
||||||
},
|
},
|
||||||
@@ -205,7 +523,7 @@ func TestEndpoint_buildHTTPRequestWithHostHeader(t *testing.T) {
|
|||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []Condition{condition},
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"Host": "example.com",
|
"Host": "example.com",
|
||||||
},
|
},
|
||||||
@@ -226,7 +544,7 @@ func TestEndpoint_buildHTTPRequestWithGraphQLEnabled(t *testing.T) {
|
|||||||
Name: "website-graphql",
|
Name: "website-graphql",
|
||||||
URL: "https://twin.sh/graphql",
|
URL: "https://twin.sh/graphql",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Conditions: []*Condition{&condition},
|
Conditions: []Condition{condition},
|
||||||
GraphQL: true,
|
GraphQL: true,
|
||||||
Body: `{
|
Body: `{
|
||||||
users(gender: "female") {
|
users(gender: "female") {
|
||||||
@@ -257,7 +575,7 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
|||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "website-health",
|
Name: "website-health",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []*Condition{&condition, &bodyCondition},
|
Conditions: []Condition{condition, bodyCondition},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
endpoint.ValidateAndSetDefaults()
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
@@ -270,25 +588,53 @@ func TestIntegrationEvaluateHealth(t *testing.T) {
|
|||||||
if !result.Success {
|
if !result.Success {
|
||||||
t.Error("Because all conditions passed, this should have been a 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) {
|
func TestIntegrationEvaluateHealthWithInvalidCondition(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 500")
|
condition := Condition("[STATUS] invalid 200")
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "website-health",
|
Name: "invalid-condition",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []*Condition{&condition},
|
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 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()
|
endpoint.ValidateAndSetDefaults()
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if result.ConditionResults[0].Success {
|
|
||||||
t.Errorf("Condition '%s' should have been a failure", condition)
|
|
||||||
}
|
|
||||||
if !result.Connected {
|
|
||||||
t.Error("Because the connection has been established, result.Connected should've been true")
|
|
||||||
}
|
|
||||||
if result.Success {
|
if result.Success {
|
||||||
t.Error("Because one of the conditions failed, success should have been false")
|
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,17 +643,17 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
|||||||
conditionBody := Condition("[BODY] == 93.184.216.34")
|
conditionBody := Condition("[BODY] == 93.184.216.34")
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "8.8.8.8",
|
URL: "1.1.1.1",
|
||||||
DNS: &DNS{
|
DNS: &DNS{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
Conditions: []*Condition{&conditionSuccess, &conditionBody},
|
Conditions: []Condition{conditionSuccess, conditionBody},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
endpoint.ValidateAndSetDefaults()
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Conditions '%s' and %s should have been a success", conditionSuccess, conditionBody)
|
t.Errorf("Conditions '%s' and '%s' should have been a success", conditionSuccess, conditionBody)
|
||||||
}
|
}
|
||||||
if !result.Connected {
|
if !result.Connected {
|
||||||
t.Error("Because the connection has been established, result.Connected should've been true")
|
t.Error("Because the connection has been established, result.Connected should've been true")
|
||||||
@@ -318,16 +664,15 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
||||||
conditionSuccess := Condition("[CONNECTED] == true")
|
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "icmp-test",
|
Name: "icmp-test",
|
||||||
URL: "icmp://127.0.0.1",
|
URL: "icmp://127.0.0.1",
|
||||||
Conditions: []*Condition{&conditionSuccess},
|
Conditions: []Condition{"[CONNECTED] == true"},
|
||||||
}
|
}
|
||||||
endpoint.ValidateAndSetDefaults()
|
endpoint.ValidateAndSetDefaults()
|
||||||
result := endpoint.EvaluateHealth()
|
result := endpoint.EvaluateHealth()
|
||||||
if !result.ConditionResults[0].Success {
|
if !result.ConditionResults[0].Success {
|
||||||
t.Errorf("Conditions '%s' should have been a success", conditionSuccess)
|
t.Errorf("Conditions '%s' should have been a success", endpoint.Conditions[0])
|
||||||
}
|
}
|
||||||
if !result.Connected {
|
if !result.Connected {
|
||||||
t.Error("Because the connection has been established, result.Connected should've been true")
|
t.Error("Because the connection has been established, result.Connected should've been true")
|
||||||
@@ -337,12 +682,20 @@ func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_DisplayName(t *testing.T) {
|
||||||
|
if endpoint := (Endpoint{Name: "n"}); endpoint.DisplayName() != "n" {
|
||||||
|
t.Error("endpoint.DisplayName() should've been 'n', but was", endpoint.DisplayName())
|
||||||
|
}
|
||||||
|
if endpoint := (Endpoint{Group: "g", Name: "n"}); endpoint.DisplayName() != "g/n" {
|
||||||
|
t.Error("endpoint.DisplayName() should've been 'g/n', but was", endpoint.DisplayName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint_getIP(t *testing.T) {
|
func TestEndpoint_getIP(t *testing.T) {
|
||||||
conditionSuccess := Condition("[CONNECTED] == true")
|
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "invalid-url-test",
|
Name: "invalid-url-test",
|
||||||
URL: "",
|
URL: "",
|
||||||
Conditions: []*Condition{&conditionSuccess},
|
Conditions: []Condition{"[CONNECTED] == true"},
|
||||||
}
|
}
|
||||||
result := &Result{}
|
result := &Result{}
|
||||||
endpoint.getIP(result)
|
endpoint.getIP(result)
|
||||||
@@ -351,26 +704,44 @@ func TestEndpoint_getIP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEndpoint_NeedsToReadBody(t *testing.T) {
|
func TestEndpoint_needsToReadBody(t *testing.T) {
|
||||||
statusCondition := Condition("[STATUS] == 200")
|
statusCondition := Condition("[STATUS] == 200")
|
||||||
bodyCondition := Condition("[BODY].status == UP")
|
bodyCondition := Condition("[BODY].status == UP")
|
||||||
bodyConditionWithLength := Condition("len([BODY].tags) > 0")
|
bodyConditionWithLength := Condition("len([BODY].tags) > 0")
|
||||||
if (&Endpoint{Conditions: []*Condition{&statusCondition}}).needsToReadBody() {
|
if (&Endpoint{Conditions: []Condition{statusCondition}}).needsToReadBody() {
|
||||||
t.Error("expected false, got true")
|
t.Error("expected false, got true")
|
||||||
}
|
}
|
||||||
if !(&Endpoint{Conditions: []*Condition{&bodyCondition}}).needsToReadBody() {
|
if !(&Endpoint{Conditions: []Condition{bodyCondition}}).needsToReadBody() {
|
||||||
t.Error("expected true, got false")
|
t.Error("expected true, got false")
|
||||||
}
|
}
|
||||||
if !(&Endpoint{Conditions: []*Condition{&bodyConditionWithLength}}).needsToReadBody() {
|
if !(&Endpoint{Conditions: []Condition{bodyConditionWithLength}}).needsToReadBody() {
|
||||||
t.Error("expected true, got false")
|
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")
|
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")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_needsToRetrieveDomainExpiration(t *testing.T) {
|
||||||
|
if (&Endpoint{Conditions: []Condition{"[STATUS] == 200"}}).needsToRetrieveDomainExpiration() {
|
||||||
|
t.Error("expected false, got true")
|
||||||
|
}
|
||||||
|
if !(&Endpoint{Conditions: []Condition{"[STATUS] == 200", "[DOMAIN_EXPIRATION] < 720h"}}).needsToRetrieveDomainExpiration() {
|
||||||
|
t.Error("expected true, got false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_needsToRetrieveIP(t *testing.T) {
|
||||||
|
if (&Endpoint{Conditions: []Condition{"[STATUS] == 200"}}).needsToRetrieveIP() {
|
||||||
|
t.Error("expected false, got true")
|
||||||
|
}
|
||||||
|
if !(&Endpoint{Conditions: []Condition{"[STATUS] == 200", "[IP] == 127.0.0.1"}}).needsToRetrieveIP() {
|
||||||
t.Error("expected true, got false")
|
t.Error("expected true, got false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
// HealthStatus is the status of Gatus
|
|
||||||
type HealthStatus struct {
|
|
||||||
// Status is the state of Gatus (UP/DOWN)
|
|
||||||
Status string `json:"status"`
|
|
||||||
|
|
||||||
// Message is an accompanying description of why the status is as reported.
|
|
||||||
// If the Status is UP, no message will be provided
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,8 @@ type Result struct {
|
|||||||
HTTPStatus int `json:"status"`
|
HTTPStatus int `json:"status"`
|
||||||
|
|
||||||
// DNSRCode is the response code of a DNS query in a human-readable format
|
// DNSRCode is the response code of a DNS query in a human-readable format
|
||||||
|
//
|
||||||
|
// Possible values: NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED
|
||||||
DNSRCode string `json:"-"`
|
DNSRCode string `json:"-"`
|
||||||
|
|
||||||
// Hostname extracted from Endpoint.URL
|
// Hostname extracted from Endpoint.URL
|
||||||
@@ -39,6 +41,9 @@ type Result struct {
|
|||||||
// CertificateExpiration is the duration before the certificate expires
|
// CertificateExpiration is the duration before the certificate expires
|
||||||
CertificateExpiration time.Duration `json:"-"`
|
CertificateExpiration time.Duration `json:"-"`
|
||||||
|
|
||||||
|
// DomainExpiration is the duration before the domain expires
|
||||||
|
DomainExpiration time.Duration `json:"-"`
|
||||||
|
|
||||||
// body is the response body
|
// body is the response body
|
||||||
//
|
//
|
||||||
// Note that this variable is only used during the evaluation of an Endpoint's health.
|
// Note that this variable is only used during the evaluation of an Endpoint's health.
|
||||||
|
|||||||
@@ -1,17 +1,60 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
// Config is the UI configuration for core.Endpoint
|
// Config is the UI configuration for core.Endpoint
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// HideHostname whether to hide the hostname in the Result
|
// HideHostname whether to hide the hostname in the Result
|
||||||
HideHostname bool `yaml:"hide-hostname"`
|
HideHostname bool `yaml:"hide-hostname"`
|
||||||
|
|
||||||
|
// 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 whether to resolve failed conditions in the Result for display in the UI
|
||||||
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
|
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
|
||||||
|
|
||||||
|
// Badge is the configuration for the badges generated
|
||||||
|
Badge *Badge `yaml:"badge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Badge struct {
|
||||||
|
ResponseTime *ResponseTime `yaml:"response-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseTime struct {
|
||||||
|
Thresholds []int `yaml:"thresholds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidBadgeResponseTimeConfig = errors.New("invalid response time badge configuration: expected parameter 'response-time' to have 5 ascending numerical values")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (config *Config) ValidateAndSetDefaults() error {
|
||||||
|
if config.Badge != nil {
|
||||||
|
if len(config.Badge.ResponseTime.Thresholds) != 5 {
|
||||||
|
return ErrInvalidBadgeResponseTimeConfig
|
||||||
|
}
|
||||||
|
for i := 4; i > 0; i-- {
|
||||||
|
if config.Badge.ResponseTime.Thresholds[i] < config.Badge.ResponseTime.Thresholds[i-1] {
|
||||||
|
return ErrInvalidBadgeResponseTimeConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.Badge = GetDefaultConfig().Badge
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultConfig retrieves the default UI configuration
|
// GetDefaultConfig retrieves the default UI configuration
|
||||||
func GetDefaultConfig() *Config {
|
func GetDefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
HideHostname: false,
|
HideHostname: false,
|
||||||
|
HideURL: false,
|
||||||
DontResolveFailedConditions: false,
|
DontResolveFailedConditions: false,
|
||||||
|
Badge: &Badge{
|
||||||
|
ResponseTime: &ResponseTime{
|
||||||
|
Thresholds: []int{50, 200, 300, 500, 750},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module github.com/TwiN/gatus/v3
|
module github.com/TwiN/gatus/v4
|
||||||
|
|
||||||
go 1.17
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/TwiN/g8 v1.3.0
|
github.com/TwiN/g8 v1.4.0
|
||||||
github.com/TwiN/gocache v1.2.4
|
github.com/TwiN/gocache/v2 v2.1.1
|
||||||
github.com/TwiN/gocache/v2 v2.0.0
|
github.com/TwiN/health v1.4.0
|
||||||
github.com/TwiN/health v1.3.0
|
github.com/TwiN/whois v1.0.0
|
||||||
github.com/coreos/go-oidc/v3 v3.1.0
|
github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
|
github.com/go-ping/ping v0.0.0-20210911151512-381826476871
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
@@ -34,7 +34,6 @@ require (
|
|||||||
github.com/prometheus/common v0.31.1 // indirect
|
github.com/prometheus/common v0.31.1 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // 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/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||||
golang.org/x/mod v0.5.1 // indirect
|
golang.org/x/mod v0.5.1 // indirect
|
||||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
|
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
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.4.0 h1:RUk5xTtxKCdMo0GGSbBVyjtAAfi2nqVbA9E0C4u5Cxo=
|
||||||
github.com/TwiN/g8 v1.3.0/go.mod h1:SiIdItS0agSUloFqdQQt/RObB2jGSq+nnE9WfFv3RIo=
|
github.com/TwiN/g8 v1.4.0/go.mod h1:ECyGJsoIb99klUfvVQoS1StgRLte9yvvPigGrHdy284=
|
||||||
github.com/TwiN/gocache v1.2.4 h1:AfJ1YRcxtQ/zZEN61URDwk/dwFG7LSRenU5qIm9dQzo=
|
github.com/TwiN/gocache/v2 v2.1.1 h1:W/GLImqa+pZVIH9pcWEn1cBgy1KU66fUcBjOnPhjuno=
|
||||||
github.com/TwiN/gocache v1.2.4/go.mod h1:BjabsQQy6z5uHDorHa4LJVPEzFeitLIDbCtdv3gc1gA=
|
github.com/TwiN/gocache/v2 v2.1.1/go.mod h1:SnUuBsrwGQeNcDG6vhkOMJnqErZM0JGjgIkuKryokYA=
|
||||||
github.com/TwiN/gocache/v2 v2.0.0 h1:CPbDNKdSJpmBkh7aWcO7D3KK1yWaMlwX+3dsBPE8/so=
|
github.com/TwiN/health v1.4.0 h1:Ts7lb4ihYDpVEbFSGAhSEZTSwuDOADnwJLFngFl4xzw=
|
||||||
github.com/TwiN/gocache/v2 v2.0.0/go.mod h1:j4MABVaia2Tp53ERWc/3l4YxkswtPjB2hQzmL/kD/VQ=
|
github.com/TwiN/health v1.4.0/go.mod h1:CSUh+ryfD2POS2vKtc/yO4IxgR58lKvQ0/8qnoPqPqs=
|
||||||
github.com/TwiN/health v1.3.0 h1:xw90rZqg0NH5MRkVHzlgtDdP+EQd43v3yMqQVtYlGHg=
|
github.com/TwiN/whois v1.0.0 h1:I+aQzXLPmhWovkFUzlPV2DdfLZUWDLrkMDlM6QwCv+Q=
|
||||||
github.com/TwiN/health v1.3.0/go.mod h1:Bt+lEvSi6C/9NWb7OoGmUmgtS4dfPeMM9EINnURv5dE=
|
github.com/TwiN/whois v1.0.0/go.mod h1:9WbCzYlR+r5eq9vbgJVh7A4H2uR2ct4wKEB0/QITJ/c=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
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/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=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
@@ -70,8 +70,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.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/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/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 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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@@ -83,7 +81,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-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 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-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/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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
@@ -149,7 +146,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
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.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/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/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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
@@ -187,13 +183,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/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-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/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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/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=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -233,17 +222,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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
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/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 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
|
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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
@@ -289,12 +273,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.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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.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 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
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-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-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-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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -318,7 +300,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-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-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-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-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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
@@ -327,7 +308,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-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-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-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 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
|
||||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
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=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@@ -351,7 +331,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/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-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-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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -362,10 +341,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-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-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-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-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-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-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -381,12 +357,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-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-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-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-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-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-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-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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -396,7 +370,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-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-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-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-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 h1:EyJTLQbOxvk8V6oDdD8ILR1BOs3nEJXThD6aqsiPNkM=
|
||||||
golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211003122950-b1ebd4e1001c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -544,12 +517,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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/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 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
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 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
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.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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v3/config"
|
"github.com/TwiN/gatus/v4/config"
|
||||||
"github.com/TwiN/gatus/v3/controller"
|
"github.com/TwiN/gatus/v4/controller"
|
||||||
"github.com/TwiN/gatus/v3/storage/store"
|
"github.com/TwiN/gatus/v4/storage/store"
|
||||||
"github.com/TwiN/gatus/v3/watchdog"
|
"github.com/TwiN/gatus/v4/watchdog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -36,7 +36,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start(cfg *config.Config) {
|
func start(cfg *config.Config) {
|
||||||
go controller.Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics)
|
go controller.Handle(cfg)
|
||||||
watchdog.Monitor(cfg)
|
watchdog.Monitor(cfg)
|
||||||
go listenToConfigurationFileChanges(cfg)
|
go listenToConfigurationFileChanges(cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
-11
@@ -1,17 +1,10 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import "log"
|
|
||||||
|
|
||||||
// BasicConfig is the configuration for Basic authentication
|
// BasicConfig is the configuration for Basic authentication
|
||||||
type BasicConfig struct {
|
type BasicConfig struct {
|
||||||
// Username is the name which will need to be used for a successful authentication
|
// Username is the name which will need to be used for a successful authentication
|
||||||
Username string `yaml:"username"`
|
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
|
// PasswordBcryptHashBase64Encoded is the base64 encoded string of the Bcrypt hash of the password to use to
|
||||||
// authenticate using basic auth.
|
// authenticate using basic auth.
|
||||||
PasswordBcryptHashBase64Encoded string `yaml:"password-bcrypt-base64"`
|
PasswordBcryptHashBase64Encoded string `yaml:"password-bcrypt-base64"`
|
||||||
@@ -19,8 +12,5 @@ type BasicConfig struct {
|
|||||||
|
|
||||||
// isValid returns whether the basic security configuration is valid or not
|
// isValid returns whether the basic security configuration is valid or not
|
||||||
func (c *BasicConfig) isValid() bool {
|
func (c *BasicConfig) isValid() bool {
|
||||||
if len(c.PasswordSha512Hash) > 0 {
|
return len(c.Username) > 0 && len(c.PasswordBcryptHashBase64Encoded) > 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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,6 @@ package security
|
|||||||
|
|
||||||
import "testing"
|
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) {
|
func TestBasicConfig_IsValidUsingBcrypt(t *testing.T) {
|
||||||
basicConfig := &BasicConfig{
|
basicConfig := &BasicConfig{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package security
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/TwiN/g8"
|
"github.com/TwiN/g8"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -82,13 +81,6 @@ func (c *Config) ApplySecurityMiddleware(api *mux.Router) error {
|
|||||||
_, _ = w.Write([]byte("Unauthorized"))
|
_, _ = w.Write([]byte("Unauthorized"))
|
||||||
return
|
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)
|
handler.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user