mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-21 23:43:27 +01:00
Sync with upstream
This commit is contained in:
commit
6bd7d272a1
29
.examples/docker-compose-mtls/certs/client/client.crt
Normal file
29
.examples/docker-compose-mtls/certs/client/client.crt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFBjCCAu6gAwIBAgIUHJXHAqywj2v25AgX7pDSZ+LX4iAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEjEQMA4GA1UEAwwHZXhhbXBsZTAeFw0yNDA0MjUwMTQ1MDFaFw0yOTA0MjQw
|
||||||
|
MTQ1MDFaMBExDzANBgNVBAMMBmNsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIP
|
||||||
|
ADCCAgoCggIBANTmRlS5BNG82mOdrhtRPIBD5U40nEW4CVFm85ZJ4Bge4Ty86juf
|
||||||
|
aoCnI6AEfwpVnJhXPzjUsMBxJFMbiCB+QTJRpxTphtK7orpbwRHjaDZNaLr1MrUO
|
||||||
|
ieADGiHw93zVDikD8FP5vG+2XWWA56hY84Ac0TR9GqPjsW0nobMgBNgsRtbYUD0B
|
||||||
|
T5QOItK180xQRn4jbys5jRnr161S+Sbg6mglz1LBFBCLmZnhZFZ8FAn87gumbnWN
|
||||||
|
etSnu9kX6iOXBIaB+3nuHOL4xmAan8tAyen6mPfkXrE5ogovjqFFMTUJOKQoJVp3
|
||||||
|
zzm/0XYANxoItFGtdjGMTl5IgI220/6kfpn6PYN7y1kYn5EI+UbobD/CuAhd94p6
|
||||||
|
aQwOXU53/l+eNH/XnTsL/32QQ6qdq8sYqevlslk1M39kKNewWYCeRzYlCVscQk14
|
||||||
|
O3fkyXrtRkz30xrzfjvJQ/VzMi+e5UlemsCuCXTVZ5YyBnuWyY+mI6lZICltZSSX
|
||||||
|
VinKzpz+t4Jl7glhKiGHaNAkBX2oLddyf280zw4Cx7nDMPs4uOHONYpm90IxEOJe
|
||||||
|
zgJ9YxPK9aaKv2AoYLbvhYyKrVT+TFqoEsbQk4vK0t0Gc1j5z4dET31CSOuxVnnU
|
||||||
|
LYwtbILFc0uZrbuOAbEbXtjPpw2OGqWagD0QpkE8TjN0Hd0ibyXyUuz5AgMBAAGj
|
||||||
|
VTBTMBEGA1UdEQQKMAiCBmNsaWVudDAdBgNVHQ4EFgQUleILTHG5lT2RhSe9H4fV
|
||||||
|
xUh0bNUwHwYDVR0jBBgwFoAUbh9Tg4oxxnHJTSaa0WLBTesYwxEwDQYJKoZIhvcN
|
||||||
|
AQELBQADggIBABq8zjRrDaljl867MXAlmbV7eJkSnaWRFct+N//jCVNnKMYaxyQm
|
||||||
|
+UG12xYP0U9Zr9vhsqwyTZTQFx/ZFiiz2zfXPtUAppV3AjE67IlKRbec3qmUhj0H
|
||||||
|
Rv20eNNWXTl1XTX5WDV5887TF+HLZm/4W2ZSBbS3V89cFhBLosy7HnBGrP0hACne
|
||||||
|
ZbdQWnnLHJMDKXkZey1H1ZLQQCQdAKGS147firj29M8uzSRHgrR6pvsNQnRT0zDL
|
||||||
|
TlTJoxyGTMaoj+1IZvRsAYMZCRb8Yct/v2i/ukIykFWUJZ+1Z3UZhGrX+gdhLfZM
|
||||||
|
jAP4VQ+vFgwD6NEXAA2DatoRqxbN1ZGJQkvnobWJdZDiYu4hBCs8ugKUTE+0iXWt
|
||||||
|
hSyrAVUspFCIeDN4xsXT5b0j2Ps4bpSAiGx+aDDTPUnd881I6JGCiIavgvdFMLCW
|
||||||
|
yOXJOZvXcNQwsndkob5fZAEqetjrARsHhQuygEq/LnPc6lWsO8O6UzYArEiKWTMx
|
||||||
|
N/5hx12Pb7aaQd1f4P3gmmHMb/YiCQK1Qy5d4v68POeqyrLvAHbvCwEMhBAbnLvw
|
||||||
|
gne3psql8s5wxhnzwYltcBUmmAw1t33CwzRBGEKifRdLGtA9pbua4G/tomcDDjVS
|
||||||
|
ChsHGebJvNxOnsQqoGgozqM2x8ScxmJzIflGxrKmEA8ybHpU0d02Xp3b
|
||||||
|
-----END CERTIFICATE-----
|
51
.examples/docker-compose-mtls/certs/client/client.key
Normal file
51
.examples/docker-compose-mtls/certs/client/client.key
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIJKQIBAAKCAgEA1OZGVLkE0bzaY52uG1E8gEPlTjScRbgJUWbzlkngGB7hPLzq
|
||||||
|
O59qgKcjoAR/ClWcmFc/ONSwwHEkUxuIIH5BMlGnFOmG0ruiulvBEeNoNk1ouvUy
|
||||||
|
tQ6J4AMaIfD3fNUOKQPwU/m8b7ZdZYDnqFjzgBzRNH0ao+OxbSehsyAE2CxG1thQ
|
||||||
|
PQFPlA4i0rXzTFBGfiNvKzmNGevXrVL5JuDqaCXPUsEUEIuZmeFkVnwUCfzuC6Zu
|
||||||
|
dY161Ke72RfqI5cEhoH7ee4c4vjGYBqfy0DJ6fqY9+ResTmiCi+OoUUxNQk4pCgl
|
||||||
|
WnfPOb/RdgA3Ggi0Ua12MYxOXkiAjbbT/qR+mfo9g3vLWRifkQj5RuhsP8K4CF33
|
||||||
|
inppDA5dTnf+X540f9edOwv/fZBDqp2ryxip6+WyWTUzf2Qo17BZgJ5HNiUJWxxC
|
||||||
|
TXg7d+TJeu1GTPfTGvN+O8lD9XMyL57lSV6awK4JdNVnljIGe5bJj6YjqVkgKW1l
|
||||||
|
JJdWKcrOnP63gmXuCWEqIYdo0CQFfagt13J/bzTPDgLHucMw+zi44c41imb3QjEQ
|
||||||
|
4l7OAn1jE8r1poq/YChgtu+FjIqtVP5MWqgSxtCTi8rS3QZzWPnPh0RPfUJI67FW
|
||||||
|
edQtjC1sgsVzS5mtu44BsRte2M+nDY4apZqAPRCmQTxOM3Qd3SJvJfJS7PkCAwEA
|
||||||
|
AQKCAgAPwAALUStib3aMkLlfpfve1VGyc8FChcySrBYbKS3zOt2Y27T3DOJuesRE
|
||||||
|
7fA5Yyn+5H1129jo87XR5s3ZnDLV4SUw2THd3H8RCwFWgcdPinHUBZhnEpial5V9
|
||||||
|
q1DzzY3gSj1OSRcVVfLE3pYaEIflvhFasQ1L0JLAq4I9OSzX5+FPEEOnWmB5Ey6k
|
||||||
|
/fbuJLDXsLwPAOadDfiFBwgNm0KxdRKdtvugBGPW9s4Fzo9rnxLmjmfKOdmQv96Y
|
||||||
|
FI/Vat0Cgmfd661RZpbDvKnTpIsLdzw3zTpAIYOzqImvCT+3AmP2qPhSdV3sPMeR
|
||||||
|
047qqyLZOVxEFXLQFiGvL4uxYUPy8k0ZI9xkgOfZ/uASozMWsHkaD04+UDi1+kw5
|
||||||
|
nfasZLvOWBW/WE/E1Rfz8IiYTeZbgTnY4CraiLrIRc0LGgD1Df4gNr25+P+LKLyK
|
||||||
|
/WW89dl6/397HOFnA7CHi7DaA8+9uZAjOWhoCNDdqAVa3QpDD/3/iRiih26bjJfH
|
||||||
|
2+sarxU8GovDZFxWd59BUP3jkukCFH+CliQy72JtLXiuPNPAWeGV9UXxtIu40sRX
|
||||||
|
Sax/TQytYi2J9NJFZFMTwVueIfzsWc8dyM+IPAYJQxN94xYKQU4+Rb/wqqHgUfjT
|
||||||
|
1ZQJb8Cmg56IDY/0EPJWQ0qgnE7TZbY2BOEYbpOzdccwUbcEjQKCAQEA8kVyw4Hw
|
||||||
|
nqcDWXjzMhOOoRoF8CNwXBvE2KBzpuAioivGcSkjkm8vLGfQYAbDOVMPFt3xlZS0
|
||||||
|
0lQm894176Kk8BiMqtyPRWWOsv4vYMBTqbehKn09Kbh6lM7d7jO7sh5iWf4jt3Bw
|
||||||
|
Sk4XhZ9oQ/kpnEKiHPymHQY3pVYEyFCGJ8mdS6g/TWiYmjMjkQDVFA4xkiyJ0S5J
|
||||||
|
NGYxI+YXtHVTVNSePKvY0h51EqTxsexAphGjXnQ3xoe6e3tVGBkeEkcZlESFD/91
|
||||||
|
0iqdc5VtKQOwy6Tj4Awk7oK5/u3tfpyIyo31LQIqreTqMO534838lpyp3CbRdvCF
|
||||||
|
QdCNpKFX1gZgmwKCAQEA4Pa9VKO3Aw95fpp0T81xNi+Js/NhdsvQyv9NI9xOKKQU
|
||||||
|
hiWxmYmyyna3zliDGlqtlw113JFTNQYl1k1yi4JQPu2gnj8te9nB0yv0RVxvbTOq
|
||||||
|
u8K1j9Xmj8XVpcKftusQsZ2xu52ONj3ZOOf22wE4Y6mdQcps+rN6XTHRBn7a5b0v
|
||||||
|
ZCvWf4CIttdIh51pZUIbZKHTU51uU7AhTCY/wEUtiHwYTT9Wiy9Lmay5Lh2s2PCz
|
||||||
|
yPE5Y970nOzlSCUl3bVgY1t0xbQtaO5AJ/iuw/vNw+YAiAIPNDUcbcK5njb//+0E
|
||||||
|
uTEtDA6SHeYfsNXGDzxipueKXFHfJLCTXnnT5/1v+wKCAQEA0pF78uNAQJSGe8B9
|
||||||
|
F3waDnmwyYvzv4q/J00l19edIniLrJUF/uM2DBFa8etOyMchKU3UCJ9MHjbX+EOd
|
||||||
|
e19QngGoWWUD/VwMkBQPF7dxv+QDZwudGmLl3+qAx+Uc8O4pq3AQmQJYBq0jEpd/
|
||||||
|
Jv0rpk3f2vPYaQebW8+MrpIWWASK+1QLWPtdD0D9W61uhVTkzth5HF9vbuSXN01o
|
||||||
|
Mwd6WxPFSJRQCihAtui3zV26vtw7sv+t7pbPhT2nsx85nMdBOzXmtQXi4Lz7RpeM
|
||||||
|
XgaAJi91g6jqfIcQo7smHVJuLib9/pWQhL2estLBTzUcocced2Mh0Y+xMofSZFF7
|
||||||
|
J2E5mwKCAQAO9npbUdRPYM0c7ZsE385C42COVobKBv5pMhfoZbPRIjC3R3SLmMwK
|
||||||
|
iWDqWZrGuvdGz79iH0xgf3suyNHwk4dQ2C9RtzQIQ9CPgiHqJx7GLaSSfn3jBkAi
|
||||||
|
me7+6nYDDZl7pth2eSFHXE/BaDRUFr2wa0ypXpRnDF78Kd8URoW6uB2Z1QycSGlP
|
||||||
|
d/w8AO1Mrdvykozix9rZuCJO1VByMme350EaijbwZQHrQ8DBX3nqp//dQqYljWPJ
|
||||||
|
uDv703S0TWcO1LtslvJaQ1aDEhhVsr7Z48dvRGvMdifg6Q29hzz5wcMJqkqrvaBc
|
||||||
|
Wr0K3v0gcEzDey0JvOxRnWj/5KyChqnXAoIBAQDq6Dsks6BjVP4Y1HaA/NWcZxUU
|
||||||
|
EZfNCTA19jIHSUiPbWzWHNdndrUq33HkPorNmFaEIrTqd/viqahr2nXpYiY/7E+V
|
||||||
|
cpn9eSxot5J8DB4VI92UG9kixxY4K7QTMKvV43Rt6BLosW/cHxW5XTNhB4JDK+TO
|
||||||
|
NlHH48fUp2qJh7/qwSikDG130RVHKwK/5Fv3NQyXTw1/n9bhnaC4eSvV39CNSeb5
|
||||||
|
rWNEZcnc9zHT2z1UespzVTxVy4hscrkssXxcCq4bOF4bnDFjfblE43o/KrVr2/Ub
|
||||||
|
jzpXQrAwXNq7pAkIpin0v40lCeTMosSgQLFqMWmtmlCpBVkyEAc9ZYXc3Vs0
|
||||||
|
-----END RSA PRIVATE KEY-----
|
29
.examples/docker-compose-mtls/certs/server/ca.crt
Normal file
29
.examples/docker-compose-mtls/certs/server/ca.crt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIE9DCCAtygAwIBAgIUCXgA3IbeA2mn8DQ0E5IxaKBLtf8wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEjEQMA4GA1UEAwwHZXhhbXBsZTAeFw0yNDA0MjUwMTE5MzRaFw0zNDA0MjMw
|
||||||
|
MTE5MzRaMBIxEDAOBgNVBAMMB2V4YW1wbGUwggIiMA0GCSqGSIb3DQEBAQUAA4IC
|
||||||
|
DwAwggIKAoICAQDLE4aTrVJrAVYksFJt5fIVhEJT5T0cLqvtDRf9hXA5Gowremsl
|
||||||
|
VJPBm4qbdImzJZCfCcbVjFEBw8h9xID1JUqRWjJ8BfTnpa4qc1e+xRtnvC+OsUeT
|
||||||
|
CCgZvK3TZ5vFsaEbRoNGuiaNq9WSTfjLwTxkK6C3Xogm9uDx73PdRob1TNK5A9mE
|
||||||
|
Ws3ZyV91+g1phKdlNMRaK+wUrjUjEMLgr0t5A5t6WKefsGrFUDaT3sye3ZxDYuEa
|
||||||
|
ljt+F8hLVyvkDBAhh6B4S5dQILjp7L3VgOsG7Hx9py1TwCbpWXZEuee/1/2OD8tA
|
||||||
|
ALsxkvRE1w4AZzLPYRL/dOMllLjROQ4VugU8GVpNU7saK5SeWBw3XHyJ9m8vne3R
|
||||||
|
cPWaZTfkwfj8NjCgi9BzBPW8/uw7XZMmQFyTj494OKM3T5JQ5jZ5XD97ONm9h+C/
|
||||||
|
oOmkcWHz6IwEUu7XV5IESxiFlrq8ByAYF98XPhn2wMMrm2OvHMOwrfw2+5U8je5C
|
||||||
|
z70p9kpiGK8qCyjbOl9im975jwFCbl7LSj3Y+0+vRlTG/JA4jNZhXsMJcAxeJpvr
|
||||||
|
pmm/IzN+uXNQzmKzBHVDw+mTUMPziRsUq4q6WrcuQFZa6kQFGNYWI/eWV8o4AAvp
|
||||||
|
HtrOGdSyU19w0QqPW0wHmhsV2XFcn6H/E1Qg6sxWpl45YWJFhNaITxm1EQIDAQAB
|
||||||
|
o0IwQDAOBgNVHQ8BAf8EBAMCAgQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
|
||||||
|
bh9Tg4oxxnHJTSaa0WLBTesYwxEwDQYJKoZIhvcNAQELBQADggIBAKvOh81Gag0r
|
||||||
|
0ipYS9aK6rp58b6jPpF6shr3xFiJQVovgSvxNS3aWolh+ZupTCC3H2Q1ZUgatak0
|
||||||
|
VyEJVO4a7Tz+1XlA6KErhnORC6HB/fgr5KEGraO3Q1uWonPal5QU8xHFStbRaXfx
|
||||||
|
hl/k4LLhIdJqcJE+XX/AL8ekZ3NPDtf9+k4V+RBuarLGuKgOtBB8+1qjSpClmW2B
|
||||||
|
DaWPlrLPOr2Sd29WOeWHifwVc6kBGpwM3g5VGdDsNX4Ba5eIG3lX2kUzJ8wNGEf0
|
||||||
|
bZxcVbTBY+D4JaV4WXoeFmajjK3EdizRpJRZw3fM0ZIeqVYysByNu/TovYLJnBPs
|
||||||
|
5AybnO4RzYONKJtZ1GtQgJyG+80/VffDJeBmHKEiYvE6mvOFEBAcU4VLU6sfwfT1
|
||||||
|
y1dZq5G9Km72Fg5kCuYDXTT+PB5VAV3Z6k819tG3TyI4hPlEphpoidRbZ+QS9tK5
|
||||||
|
RgHah9EJoM7tDAN/mUVHJHQhhLJDBn+iCBYgSJVLwoE+F39NO9oFPD/ZxhJkbk9b
|
||||||
|
LkFnpjrVbwD1CNnawX3I2Eytg1IbbzyviQIbpSAEpotk9pCLMAxTR3a08wrVMwst
|
||||||
|
2XVSrgK0uUKsZhCIc+q21k98aeNIINor15humizngyBWYOk8SqV84ZNcD6VlM3Qv
|
||||||
|
ShSKoAkdKxcGG1+MKPt5b7zqvTo8BBPM
|
||||||
|
-----END CERTIFICATE-----
|
30
.examples/docker-compose-mtls/certs/server/server.crt
Normal file
30
.examples/docker-compose-mtls/certs/server/server.crt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFDjCCAvagAwIBAgITc5Ejz7RzBJ2/PcUMsVhj41RtQDANBgkqhkiG9w0BAQsF
|
||||||
|
ADASMRAwDgYDVQQDDAdleGFtcGxlMB4XDTI0MDQyNTAxNDQ1N1oXDTI5MDQyNDAx
|
||||||
|
NDQ1N1owEDEOMAwGA1UEAwwFbmdpbngwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
|
||||||
|
ggIKAoICAQCgbLBnVrBdRkBF2XmJgDTiRqWFPQledzCrkHF4eiUvtEytJhkpoRv2
|
||||||
|
+SiRPsjCo3XjwcgQIgSy1sHUV8Sazn7V5ux/XBRovhdhUivzI8JSRYj6qwqdUnOy
|
||||||
|
dG1ZEy/VRLsIVfoFB0jKJrZCXMT256xkYTlsgPePDsduO7IPPrTN0/I/qBvINFet
|
||||||
|
zgWCl2qlZgF4c/MHljo2TR1KlBv0RJUZbfXPwemUazyMrh/MfQHaHE5pfrmMWFGA
|
||||||
|
6yLYHEhG+fy5d3F/1+4J24D2j7deIFmmuJMPSlAPt1UjDm7M/bmoTxDG+1MRXSnN
|
||||||
|
647EzzS0TFZspHe2+yBbw6j0MMiWMzNZX2iXGVcswXwrphe7ro6OITynM76gDTuM
|
||||||
|
ISYXKYHayqW0rHFRlKxMcnmrpf5tBuK7XKyoQv/LbFKI1e+j1bNVe7OZtC88EWRc
|
||||||
|
SD8WDLqo/3rsxJkRXRW/49hO1nynHrknXJEpZeRnTyglS+VCzXYD0XzwzPKN7CyN
|
||||||
|
CHpYpOcWrAMF+EJnE4WRVyJAAt4C1pGhiwn0yCvLEGXXedI/rR5zmUBKitSe7oMT
|
||||||
|
J82H/VaGtwH0lOD9Jjsv9cb+s1c3tChPDKvgGGDaFnlehKg9TM7p+xc9mnEsitfv
|
||||||
|
ovSGzYHk29nQu/S4QrPfWuCNwM2vP9OQ+VJyzDzSyH8iuPPmkfmK5wIDAQABo18w
|
||||||
|
XTAbBgNVHREEFDASggVuZ2lueIIJbG9jYWxob3N0MB0GA1UdDgQWBBT89oboWPBC
|
||||||
|
oNsSbaNquzrjTza6xDAfBgNVHSMEGDAWgBRuH1ODijHGcclNJprRYsFN6xjDETAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAgEAeg8QwBTne1IGZMDvIGgs95lifzuTXGVQWEid7VVp
|
||||||
|
MmXGRYsweb0MwTUq3gSUc+3OPibR0i5HCJRR04H4U+cIjR6em1foIV/bW6nTaSls
|
||||||
|
xQAj92eMmzOo/KtOYqMnk//+Da5NvY0myWa/8FgJ7rK1tOZYiTZqFOlIsaiQMHgp
|
||||||
|
/PEkZBP5V57h0PY7T7tEj4SCw3DJ6qzzIdpD8T3+9kXd9dcrrjbivBkkJ23agcG5
|
||||||
|
wBcI862ELNJOD7p7+OFsv7IRsoXXYrydaDg8OJQovh4RccRqVEQu3hZdi7cPb8xJ
|
||||||
|
G7Gxn8SfSVcPg/UObiggydMl8E8QwqWAzJHvl1KUECd5QG6eq984JTR7zQB2iGb6
|
||||||
|
1qq+/d9uciuB2YY2h/0rl3Fjy6J6k3fpQK577TlJjZc0F4WH8fW5bcsyGTszxQLI
|
||||||
|
jQ6FuSOr55lZ9O3R3+95tAdJTrWsxX7j7xMIAXSYrfNt5HM91XNhqISF4SIZOBB6
|
||||||
|
enVrrJ/oCFqVSbYf6RVQz3XmPEEMh+k9KdwvIvwoS9NivLD3QH0RjhTyzHbf+LlR
|
||||||
|
rWM46XhmBwajlpnIuuMp6jZcXnbhTO1SheoRVMdijcnW+zrmx5oyn3peCfPqOVLz
|
||||||
|
95YfJUIFCt+0p/87/0Mm76uVemK6kFKZJQPnfbAdsKF7igPZfUQx6wZZP1qK9ZEU
|
||||||
|
eOk=
|
||||||
|
-----END CERTIFICATE-----
|
51
.examples/docker-compose-mtls/certs/server/server.key
Normal file
51
.examples/docker-compose-mtls/certs/server/server.key
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIJKQIBAAKCAgEAoGywZ1awXUZARdl5iYA04kalhT0JXncwq5BxeHolL7RMrSYZ
|
||||||
|
KaEb9vkokT7IwqN148HIECIEstbB1FfEms5+1ebsf1wUaL4XYVIr8yPCUkWI+qsK
|
||||||
|
nVJzsnRtWRMv1US7CFX6BQdIyia2QlzE9uesZGE5bID3jw7HbjuyDz60zdPyP6gb
|
||||||
|
yDRXrc4FgpdqpWYBeHPzB5Y6Nk0dSpQb9ESVGW31z8HplGs8jK4fzH0B2hxOaX65
|
||||||
|
jFhRgOsi2BxIRvn8uXdxf9fuCduA9o+3XiBZpriTD0pQD7dVIw5uzP25qE8QxvtT
|
||||||
|
EV0pzeuOxM80tExWbKR3tvsgW8Oo9DDIljMzWV9olxlXLMF8K6YXu66OjiE8pzO+
|
||||||
|
oA07jCEmFymB2sqltKxxUZSsTHJ5q6X+bQbiu1ysqEL/y2xSiNXvo9WzVXuzmbQv
|
||||||
|
PBFkXEg/Fgy6qP967MSZEV0Vv+PYTtZ8px65J1yRKWXkZ08oJUvlQs12A9F88Mzy
|
||||||
|
jewsjQh6WKTnFqwDBfhCZxOFkVciQALeAtaRoYsJ9MgryxBl13nSP60ec5lASorU
|
||||||
|
nu6DEyfNh/1WhrcB9JTg/SY7L/XG/rNXN7QoTwyr4Bhg2hZ5XoSoPUzO6fsXPZpx
|
||||||
|
LIrX76L0hs2B5NvZ0Lv0uEKz31rgjcDNrz/TkPlScsw80sh/Irjz5pH5iucCAwEA
|
||||||
|
AQKCAgADiEEeFV+OvjQ+FXrCl0sSzGFqnJxvMwqkTGrjLzVQZpTlnxggvYZjGrtU
|
||||||
|
71/2QSkgWazxBf66fVYJOeF/Uxqh1RLR/xIH+F+FagzDrr7hltxcQJXcPuuDO2MI
|
||||||
|
+g4skPXZSiNWJwHoSY/ryCUiFpnKIAXmqLRKtxWXDMNv6H6MpaUI18e80cI4dnfS
|
||||||
|
l0jm2Wcg4tSwDxO7DFmfwcEX0MbDp5Mo/ukIto+/vTnAA+Sdi9ACLKMjPvKUdxju
|
||||||
|
TzkcLvbskn+yQ+ve1bFyPFnaPbYboKbESGuY3P2H5xJzewayeQMyjmgW0slP2mbr
|
||||||
|
WHCdo6ynebuVENR2kMlQjx5riDcSMMX5TLGPgNL7ZBf2b52mUgFyQb27eO2WXeyH
|
||||||
|
YLtInlKA44bdi76sDK+s8zYywZnxsUy7xrKhHE5rqz964EfoLRcY/fCm7XnMo6uK
|
||||||
|
VviBtdPebsMqkZOUKSaYSRpUgXILTud5FD+m68FeVjUvQFQqHYEa3gx+rAIjKBIn
|
||||||
|
082NzfDZSHVsvG+iB5q+37R8C0/YUzSb3TXys5pA82YsjIFeQiVE4hrV1yeNIZf6
|
||||||
|
2iaPD/r5H3vt0rFEDINZafC+6bTTRQoq8TOCZFh/Lu+ynXKOPrVUF8/y3sd8+T2v
|
||||||
|
kRDOL37reUotjE1lbO4RhLgHbeWHlT/PPnF7RDKCe6/erg2MqQKCAQEAy3f8B6I8
|
||||||
|
7CP4CZmMDWwHWsjMS/HGZgvPPbmWhaeZZmFyYi7I8MruJPhlhlw6YoUIV9Vvp8zE
|
||||||
|
eLtDvZ5WXuL38aRElWzNyrhrU1/vH4pkaFk+OgRcaleGUof+go0lE8BIYnWoWovo
|
||||||
|
/F7lQMQmHY4SuwF4oj6dpus7jMm41PQqDTsjofdLgwVAGy30LIkVt8qYha77sL8N
|
||||||
|
0ohXomDGik0nVa+i2mOJ0UuooGYF8WhujzVcELcerYvvg9kFDqJaEXdfTx4DRwiz
|
||||||
|
6f5gSbZHME7moqEkcJRtwj8TXSJYRHTI8ngS0xzyV0u2RL3FOxTcgikJIkmU6W3L
|
||||||
|
IcbP6XVlrCdoswKCAQEAydfBcsYcS2mMqCOdKkGVj6zBriT78/5dtPYeId9WkrnX
|
||||||
|
1vz6ErjHQ8vZkduvCm3KkijQvva+DFV0sv24qTyA2BIoDUJdk7cY962nR4Q9FHTX
|
||||||
|
Dkn1kgeKg4TtNdgo2KsIUn7bCibKASCExo6rO3PWiQyF+jTJVDD3rXx7+7N7WJaz
|
||||||
|
zTVt6BNOWoIjTufdXfRWt3wi0H6sSkqvRWoIAaguXkKXH7oBx0gKs+oAVovFvg7A
|
||||||
|
LLEtTszsv2LmbpGWaiT3Ny215mA0ZGI9T4utK7oUgd+DlV0+vj5tFfsye4COpCyG
|
||||||
|
V/ZQ7CBbxHDDak3R3fYy5pOwmh6814wHMyKKfdGm/QKCAQEAiW4Pk3BnyfA5lvJZ
|
||||||
|
gK9ZAF7kbt9tbHvJjR2Pp9Meb+KeCecj3lCTLfGBUZF19hl5GyqU8jgC9LE3/hm2
|
||||||
|
qPyREGwtzufg0G5kP7pqn1kwnLK6ryFG8qUPmys0IyYGxyJ3QdnKzu31fpDyNB7I
|
||||||
|
x+mwiRNjUeMNRTNZ06xk5aHNzYYGeV25aVPgivstE++79ZooDxOz+Rvy0CM7XfgT
|
||||||
|
4lJeoSeyzeOxsOZzjXObzAUHuD8IYlntpLcCHoI1Qj8yqt2ASMYy3IXqT8B7dQ5j
|
||||||
|
YyPH8Ez7efcnc656+8s453QiTnP/8wx4O7Jt+FxdnZxnnJrvCnO82zZHoBbTVBLx
|
||||||
|
i6hKtQKCAQA0j3SWmLRBhwjTuAJzQITb1xbQbF0X2oM4XmbWVzxKFQ75swLD4U4y
|
||||||
|
f2D2tIhOZOy9RtelAsfWmmI7QgrWNyUuHvxDB6cqkiF0Tcoju3HUY+CknenOzxvo
|
||||||
|
x7KltNZeJZuTL+mGKTetN3Sb6Ab7Al05bwNsdlZ/EAlPKf13O/PAy+2iYGlwZ6ad
|
||||||
|
twnOwF5K2xfBzBecx3/CENS3dLcFB3CbpyeHYX6ZEE+JLkRMRTWHGnw8px6vSHnW
|
||||||
|
FMEAxfSvS1T9D3Awv5ilE1f34N2FZ31znGq9eHygOc1aTgGFW6LJabbKLSBBfOOo
|
||||||
|
sdyRUBZ4gGYc2RTB7YMrdhFh5Xq+7NtZAoIBAQCOJ3CLecp/rS+lGy7oyx4f6QDd
|
||||||
|
zH/30Y/uvXLPUj+Ljg9bMTG9chjaKfyApXv6rcQI0d6wrqAunNl1b3opBQjsGCSt
|
||||||
|
bpBV/rGg3sl752og6KU1PCZ2KkVYPjugNhqPGonNh8tlw+1xFyBdt0c68g/auIHq
|
||||||
|
WaT5tWVfP01Ri43RjyCgNtJ2TJUzbA40BteDHPWKeM1lZ6e92fJTp5IjQ/Okc41u
|
||||||
|
Elr7p22fx/N04JTX9G6oGdxM7Gh2Uf4i4PnNOi+C3xqLrtUEi/OLof2UHlatypt9
|
||||||
|
pix0bXJtZE7WfFfesQIxGffVBhgN3UgqhAf2wquHgm1O17JXrmkR6JSYNpKc
|
||||||
|
-----END RSA PRIVATE KEY-----
|
13
.examples/docker-compose-mtls/config/config.yaml
Normal file
13
.examples/docker-compose-mtls/config/config.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
endpoints:
|
||||||
|
- name: example
|
||||||
|
url: https://nginx
|
||||||
|
interval: 30s
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
client:
|
||||||
|
# mtls
|
||||||
|
insecure: true
|
||||||
|
tls:
|
||||||
|
certificate-file: /certs/client.crt
|
||||||
|
private-key-file: /certs/client.key
|
||||||
|
renegotiation: once
|
27
.examples/docker-compose-mtls/docker-compose.yml
Normal file
27
.examples/docker-compose-mtls/docker-compose.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:stable
|
||||||
|
volumes:
|
||||||
|
- ./certs/server:/etc/nginx/certs
|
||||||
|
- ./nginx:/etc/nginx/conf.d
|
||||||
|
ports:
|
||||||
|
- "8443:443"
|
||||||
|
networks:
|
||||||
|
- mtls
|
||||||
|
|
||||||
|
gatus:
|
||||||
|
image: twinproduction/gatus:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
- ./certs/client:/certs
|
||||||
|
environment:
|
||||||
|
- GATUS_CONFIG_PATH=/config
|
||||||
|
networks:
|
||||||
|
- mtls
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mtls:
|
16
.examples/docker-compose-mtls/nginx/default.conf
Normal file
16
.examples/docker-compose-mtls/nginx/default.conf
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/server.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/server.key;
|
||||||
|
ssl_client_certificate /etc/nginx/certs/ca.crt;
|
||||||
|
ssl_verify_client on;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
if ($ssl_client_verify != SUCCESS) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
}
|
BIN
.github/assets/gitea-alerts.png
vendored
Normal file
BIN
.github/assets/gitea-alerts.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 638 KiB |
BIN
.github/assets/teams-workflows-alerts.png
vendored
Normal file
BIN
.github/assets/teams-workflows-alerts.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: 1.22.2
|
||||||
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
||||||
ref: "${{ github.event.inputs.ref || 'master' }}"
|
ref: "${{ github.event.inputs.ref || 'master' }}"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
2
.github/workflows/publish-experimental.yml
vendored
2
.github/workflows/publish-experimental.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
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@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
pull: true
|
pull: true
|
||||||
|
4
.github/workflows/publish-latest-to-ghcr.yml
vendored
4
.github/workflows/publish-latest-to-ghcr.yml
vendored
@ -30,9 +30,9 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.IMAGE_REPOSITORY }}:latest
|
tags: ${{ env.IMAGE_REPOSITORY }}:latest
|
||||||
|
4
.github/workflows/publish-latest.yml
vendored
4
.github/workflows/publish-latest.yml
vendored
@ -26,9 +26,9 @@ jobs:
|
|||||||
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@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.IMAGE_REPOSITORY }}:latest
|
tags: ${{ env.IMAGE_REPOSITORY }}:latest
|
||||||
|
@ -26,9 +26,9 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
4
.github/workflows/publish-release.yml
vendored
4
.github/workflows/publish-release.yml
vendored
@ -23,9 +23,9 @@ jobs:
|
|||||||
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@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.21
|
go-version: 1.22.2
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build binary to make sure it works
|
- name: Build binary to make sure it works
|
||||||
run: go build
|
run: go build
|
||||||
@ -28,7 +28,7 @@ jobs:
|
|||||||
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
||||||
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||||
- name: Codecov
|
- name: Codecov
|
||||||
uses: codecov/codecov-action@v4.3.0
|
uses: codecov/codecov-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
188
README.md
188
README.md
@ -1,14 +1,12 @@
|
|||||||
[![Gatus](.github/assets/logo-with-dark-text.png)](https://gatus.io)
|
[![Gatus](.github/assets/logo-with-dark-text.png)](https://gatus.io)
|
||||||
|
|
||||||
![test](https://github.com/TwiN/gatus/workflows/test/badge.svg?branch=master)
|
![test](https://github.com/TwiN/gatus/workflows/test/badge.svg?branch=master)
|
||||||
[![Join Discord server](https://img.shields.io/discord/442432928614449155.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/ka9RySaQ9K)
|
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/gatus?)](https://goreportcard.com/report/github.com/TwiN/gatus)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/TwiN/gatus?)](https://goreportcard.com/report/github.com/TwiN/gatus)
|
||||||
[![codecov](https://codecov.io/gh/TwiN/gatus/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/gatus)
|
[![codecov](https://codecov.io/gh/TwiN/gatus/branch/master/graph/badge.svg)](https://codecov.io/gh/TwiN/gatus)
|
||||||
[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/gatus.svg)](https://github.com/TwiN/gatus)
|
[![Go version](https://img.shields.io/github/go-mod/go-version/TwiN/gatus.svg)](https://github.com/TwiN/gatus)
|
||||||
[![Docker pulls](https://img.shields.io/docker/pulls/twinproduction/gatus.svg)](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
[![Docker pulls](https://img.shields.io/docker/pulls/twinproduction/gatus.svg)](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
||||||
[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow&style=social)](https://github.com/TwiN)
|
[![Follow TwiN](https://img.shields.io/github/followers/TwiN?label=Follow&style=social)](https://github.com/TwiN)
|
||||||
|
|
||||||
|
|
||||||
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
Gatus is a developer-oriented health dashboard that gives you the ability to monitor your services using HTTP, ICMP, TCP, and even DNS
|
||||||
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
queries as well as evaluate the result of said queries by using a list of conditions on values like the status code,
|
||||||
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
the response time, the certificate expiration, the body and many others. The icing on top is that each of these health
|
||||||
@ -55,6 +53,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Alerting](#alerting)
|
- [Alerting](#alerting)
|
||||||
- [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 Gitea alerts](#configuring-gitea-alerts)
|
||||||
- [Configuring GitHub alerts](#configuring-github-alerts)
|
- [Configuring GitHub alerts](#configuring-github-alerts)
|
||||||
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
|
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
|
||||||
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
||||||
@ -68,11 +67,13 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
||||||
- [Configuring Pushover alerts](#configuring-pushover-alerts)
|
- [Configuring Pushover alerts](#configuring-pushover-alerts)
|
||||||
- [Configuring Slack alerts](#configuring-slack-alerts)
|
- [Configuring Slack alerts](#configuring-slack-alerts)
|
||||||
- [Configuring Teams alerts](#configuring-teams-alerts)
|
- [Configuring Teams alerts *(Deprecated)*](#configuring-teams-alerts-deprecated)
|
||||||
|
- [Configuring Teams Workflow alerts](#configuring-teams-workflow-alerts)
|
||||||
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
||||||
- [Configuring Twilio alerts](#configuring-twilio-alerts)
|
- [Configuring Twilio alerts](#configuring-twilio-alerts)
|
||||||
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
|
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
|
||||||
- [Configuring custom alerts](#configuring-custom-alerts)
|
- [Configuring custom alerts](#configuring-custom-alerts)
|
||||||
|
- [Configuring Zulip alerts](#configuring-zulip-alerts)
|
||||||
- [Setting a default alert](#setting-a-default-alert)
|
- [Setting a default alert](#setting-a-default-alert)
|
||||||
- [Maintenance](#maintenance)
|
- [Maintenance](#maintenance)
|
||||||
- [Security](#security)
|
- [Security](#security)
|
||||||
@ -110,6 +111,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Configuring a startup delay](#configuring-a-startup-delay)
|
- [Configuring a startup delay](#configuring-a-startup-delay)
|
||||||
- [Keeping your configuration small](#keeping-your-configuration-small)
|
- [Keeping your configuration small](#keeping-your-configuration-small)
|
||||||
- [Proxy client configuration](#proxy-client-configuration)
|
- [Proxy client configuration](#proxy-client-configuration)
|
||||||
|
- [How to fix 431 Request Header Fields Too Large error](#how-to-fix-431-request-header-fields-too-large-error)
|
||||||
- [Badges](#badges)
|
- [Badges](#badges)
|
||||||
- [Uptime](#uptime)
|
- [Uptime](#uptime)
|
||||||
- [Health](#health)
|
- [Health](#health)
|
||||||
@ -307,12 +309,13 @@ external-endpoints:
|
|||||||
|
|
||||||
To push the status of an external endpoint, the request would have to look like this:
|
To push the status of an external endpoint, the request would have to look like this:
|
||||||
```
|
```
|
||||||
POST /api/v1/endpoints/{key}/external?success={success}
|
POST /api/v1/endpoints/{key}/external?success={success}&error={error}
|
||||||
```
|
```
|
||||||
Where:
|
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 `-`.
|
||||||
- Using the example configuration above, the key would be `core_ext-ep-test`.
|
- Using the example configuration above, the key would be `core_ext-ep-test`.
|
||||||
- `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
|
- `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
|
||||||
|
- `{error}`: a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful, it can be omitted or left empty.
|
||||||
|
|
||||||
You must also pass the token as a `Bearer` token in the `Authorization` header.
|
You must also pass the token as a `Bearer` token in the `Authorization` header.
|
||||||
|
|
||||||
@ -565,7 +568,8 @@ endpoints:
|
|||||||
| `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.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
|
| `alerting.pushover` | Configuration for alerts of type `pushover`. <br />See [Configuring Pushover alerts](#configuring-pushover-alerts). | `{}` |
|
||||||
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
| `alerting.slack` | Configuration for alerts of type `slack`. <br />See [Configuring Slack alerts](#configuring-slack-alerts). | `{}` |
|
||||||
| `alerting.teams` | Configuration for alerts of type `teams`. <br />See [Configuring Teams alerts](#configuring-teams-alerts). | `{}` |
|
| `alerting.teams` | Configuration for alerts of type `teams`. *(Deprecated)* <br />See [Configuring Teams alerts](#configuring-teams-alerts-deprecated). | `{}` |
|
||||||
|
| `alerting.teams-workflows` | Configuration for alerts of type `teams-workflows`. <br />See [Configuring Teams Workflow alerts](#configuring-teams-workflow-alerts). | `{}` |
|
||||||
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
|
| `alerting.telegram` | Configuration for alerts of type `telegram`. <br />See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
|
||||||
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
|
| `alerting.twilio` | Settings for alerts of type `twilio`. <br />See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
|
||||||
|
|
||||||
@ -662,8 +666,45 @@ endpoints:
|
|||||||
|
|
||||||
> ⚠ Some mail servers are painfully slow.
|
> ⚠ Some mail servers are painfully slow.
|
||||||
|
|
||||||
|
#### Configuring Gitea alerts
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:---------------------------------|:-----------------------------------------------------------------------------------------------------------|:--------------|
|
||||||
|
| `alerting.gitea` | Configuration for alerts of type `gitea` | `{}` |
|
||||||
|
| `alerting.gitea.repository-url` | Gitea repository URL (e.g. `https://gitea.com/TwiN/example`) | Required `""` |
|
||||||
|
| `alerting.gitea.token` | Personal access token to use for authentication. <br />Must have at least RW on issues and RO on metadata. | Required `""` |
|
||||||
|
| `alerting.github.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||||
|
|
||||||
|
The Gitea alerting provider creates an issue prefixed with `alert(gatus):` and suffixed with the endpoint's display
|
||||||
|
name for each alert. If `send-on-resolved` is set to `true` on the endpoint alert, the issue will be automatically
|
||||||
|
closed when the alert is resolved.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
gitea:
|
||||||
|
repository-url: "https://gitea.com/TwiN/test"
|
||||||
|
token: "349d63f16......"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: example
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 75"
|
||||||
|
alerts:
|
||||||
|
- type: gitea
|
||||||
|
failure-threshold: 2
|
||||||
|
success-threshold: 3
|
||||||
|
send-on-resolved: true
|
||||||
|
description: "Everything's burning AAAAAHHHHHHHHHHHHHHH"
|
||||||
|
```
|
||||||
|
|
||||||
|
![Gitea alert](.github/assets/gitea-alerts.png)
|
||||||
|
|
||||||
#### Configuring GitHub alerts
|
#### Configuring GitHub alerts
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:---------------------------------|:-----------------------------------------------------------------------------------------------------------|:--------------|
|
|:---------------------------------|:-----------------------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.github` | Configuration for alerts of type `github` | `{}` |
|
| `alerting.github` | Configuration for alerts of type `github` | `{}` |
|
||||||
@ -699,7 +740,6 @@ endpoints:
|
|||||||
|
|
||||||
![GitHub alert](.github/assets/github-alerts.png)
|
![GitHub alert](.github/assets/github-alerts.png)
|
||||||
|
|
||||||
|
|
||||||
#### Configuring GitLab alerts
|
#### Configuring GitLab alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------------|:--------------------------------------------------------------------------------------------------------------------|:--------------|
|
|:------------------------------------|:--------------------------------------------------------------------------------------------------------------------|:--------------|
|
||||||
@ -879,6 +919,7 @@ endpoints:
|
|||||||
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:--------------|
|
||||||
| `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.channel` | Mattermost channel name override (optional) | `""` |
|
||||||
| `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` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
@ -946,12 +987,16 @@ endpoints:
|
|||||||
|
|
||||||
#### Configuring Ntfy alerts
|
#### Configuring Ntfy alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:------------------------------|:-------------------------------------------------------------------------------------------|:------------------|
|
|:---------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------|
|
||||||
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
|
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
|
||||||
| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
|
| `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.url` | The URL of the target server | `https://ntfy.sh` |
|
||||||
| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
|
| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
|
||||||
|
| `alerting.ntfy.email` | E-mail address for additional e-mail notifications | `""` |
|
||||||
|
| `alerting.ntfy.click` | Website opened when notification is clicked | `""` |
|
||||||
| `alerting.ntfy.priority` | The priority of the alert | `3` |
|
| `alerting.ntfy.priority` | The priority of the alert | `3` |
|
||||||
|
| `alerting.ntfy.disable-firebase` | Whether message push delivery via firebase should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#disable-firebase) | `false` |
|
||||||
|
| `alerting.ntfy.disable-cache` | Whether server side message caching should be disabled. [ntfy.sh defaults to enabled](https://docs.ntfy.sh/publish/#message-caching) | `false` |
|
||||||
| `alerting.ntfy.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
| `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
|
[ntfy](https://github.com/binwiederhier/ntfy) is an amazing project that allows you to subscribe to desktop
|
||||||
@ -1134,20 +1179,29 @@ Here's an example of what the notifications look like:
|
|||||||
![Slack notifications](.github/assets/slack-alerts.png)
|
![Slack notifications](.github/assets/slack-alerts.png)
|
||||||
|
|
||||||
|
|
||||||
#### Configuring Teams alerts
|
#### Configuring Teams alerts *(Deprecated)*
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **Deprecated:** Office 365 Connectors within Microsoft Teams are being retired ([Source: Microsoft DevBlog](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/)).
|
||||||
|
> Existing connectors will continue to work until December 2025. The new [Teams Workflow Alerts](#configuring-teams-workflow-alerts) should be used with Microsoft Workflows instead of this legacy configuration.
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------------|
|
||||||
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
|
||||||
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
|
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
|
||||||
| `alerting.teams.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
| `alerting.teams.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
| `alerting.teams.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
| `alerting.teams.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
|
| `alerting.teams.title` | Title of the notification | `"🚨 Gatus"` |
|
||||||
| `alerting.teams.overrides[].group` | Endpoint group for which the configuration will be overridden by this 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 | `""` |
|
| `alerting.teams.overrides[].webhook-url` | Teams Webhook URL | `""` |
|
||||||
|
| `alerting.teams.client.insecure` | Whether to skip TLS verification | `false` |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
teams:
|
teams:
|
||||||
webhook-url: "https://********.webhook.office.com/webhookb2/************"
|
webhook-url: "https://********.webhook.office.com/webhookb2/************"
|
||||||
|
client:
|
||||||
|
insecure: false
|
||||||
# You can also add group-specific to keys, which will
|
# You can also add group-specific to keys, which will
|
||||||
# override the to key above for the specified groups
|
# override the to key above for the specified groups
|
||||||
overrides:
|
overrides:
|
||||||
@ -1184,16 +1238,75 @@ Here's an example of what the notifications look like:
|
|||||||
|
|
||||||
![Teams notifications](.github/assets/teams-alerts.png)
|
![Teams notifications](.github/assets/teams-alerts.png)
|
||||||
|
|
||||||
|
#### Configuring Teams Workflow alerts
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This alert is compatible with Workflows for Microsoft Teams. To setup the workflow and get the webhook URL, follow the [Microsoft Documentation](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498).
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:---------------------------------------------------|:-------------------------------------------------------------------------------------------|:-------------------|
|
||||||
|
| `alerting.teams-workflows` | Configuration for alerts of type `teams` | `{}` |
|
||||||
|
| `alerting.teams-workflows.webhook-url` | Teams Webhook URL | Required `""` |
|
||||||
|
| `alerting.teams-workflows.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
| `alerting.teams-workflows.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
|
| `alerting.teams-workflows.title` | Title of the notification | `"⛑ Gatus"` |
|
||||||
|
| `alerting.teams-workflows.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
|
| `alerting.teams-workflows.overrides[].webhook-url` | Teams WorkFlow Webhook URL | `""` |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
teams-workflows:
|
||||||
|
webhook-url: "https://********.webhook.office.com/webhookb2/************"
|
||||||
|
# You can also add group-specific to keys, which will
|
||||||
|
# override the to key above for the specified groups
|
||||||
|
overrides:
|
||||||
|
- group: "core"
|
||||||
|
webhook-url: "https://********.webhook.office.com/webhookb3/************"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
interval: 30s
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
|
alerts:
|
||||||
|
- type: teams-workflows
|
||||||
|
description: "healthcheck failed"
|
||||||
|
send-on-resolved: true
|
||||||
|
|
||||||
|
- name: back-end
|
||||||
|
group: core
|
||||||
|
url: "https://example.org/"
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[CERTIFICATE_EXPIRATION] > 48h"
|
||||||
|
alerts:
|
||||||
|
- type: teams-workflows
|
||||||
|
description: "healthcheck failed"
|
||||||
|
send-on-resolved: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's an example of what the notifications look like:
|
||||||
|
|
||||||
|
![Teams Workflow notifications](.github/assets/teams-workflows-alerts.png)
|
||||||
|
|
||||||
|
|
||||||
#### 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.api-url` | Telegram API URL | `https://api.telegram.org` |
|
| `alerting.telegram.api-url` | Telegram API URL | `https://api.telegram.org` |
|
||||||
| `alerting.telegram.client` | Client configuration. <br />See [Client configuration](#client-configuration). | `{}` |
|
| `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 |
|
| `alerting.telegram.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
| `alerting.telegram.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||||
|
| `alerting.telegram.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
|
| `alerting.telegram.overrides[].token` | Telegram Bot Token for override default value | `""` |
|
||||||
|
| `alerting.telegram.overrides[].id` | Telegram User ID for override default value | `""` |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
alerting:
|
alerting:
|
||||||
@ -1315,6 +1428,7 @@ Furthermore, you may use the following placeholders in the body (`alerting.custo
|
|||||||
- `[ENDPOINT_NAME]` (resolved from `endpoints[].name`)
|
- `[ENDPOINT_NAME]` (resolved from `endpoints[].name`)
|
||||||
- `[ENDPOINT_GROUP]` (resolved from `endpoints[].group`)
|
- `[ENDPOINT_GROUP]` (resolved from `endpoints[].group`)
|
||||||
- `[ENDPOINT_URL]` (resolved from `endpoints[].url`)
|
- `[ENDPOINT_URL]` (resolved from `endpoints[].url`)
|
||||||
|
- `[RESULT_ERRORS]` (resolved from the health evaluation of a given health check)
|
||||||
|
|
||||||
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.
|
||||||
@ -1329,7 +1443,7 @@ alerting:
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
body: |
|
body: |
|
||||||
{
|
{
|
||||||
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_GROUP] - [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
|
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_GROUP] - [ENDPOINT_NAME] - [ALERT_DESCRIPTION] - [RESULT_ERRORS]"
|
||||||
}
|
}
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@ -1450,6 +1564,42 @@ endpoints:
|
|||||||
- type: pagerduty
|
- type: pagerduty
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Configuring Zulip alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:-----------------------------------------|:------------------------------------------------------------------------------------|:------------------------------------|
|
||||||
|
| `alerting.zulip` | Configuration for alerts of type `discord` | `{}` |
|
||||||
|
| `alerting.zulip.bot-email` | Bot Email | Required `""` |
|
||||||
|
| `alerting.zulip.bot-api-key` | Bot API key | Required `""` |
|
||||||
|
| `alerting.zulip.domain` | Full organization domain (e.g.: yourZulipDomain.zulipchat.com) | Required `""` |
|
||||||
|
| `alerting.zulip.channel-id` | The channel ID where Gatus will send the alerts | Required `""` |
|
||||||
|
| `alerting.zulip.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||||
|
| `alerting.zulip.overrides[].bot-email` | . | `""` |
|
||||||
|
| `alerting.zulip.overrides[].bot-api-key` | . | `""` |
|
||||||
|
| `alerting.zulip.overrides[].domain` | . | `""` |
|
||||||
|
| `alerting.zulip.overrides[].channel-id` | . | `""` |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
zulip:
|
||||||
|
bot-email: gatus-bot@some.zulip.org
|
||||||
|
bot-api-key: "********************************"
|
||||||
|
domain: some.zulip.org
|
||||||
|
channel-id: 123456
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
interval: 5m
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
|
alerts:
|
||||||
|
- type: zulip
|
||||||
|
description: "healthcheck failed"
|
||||||
|
send-on-resolved: true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### 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.
|
||||||
@ -1460,15 +1610,15 @@ To do that, you'll have to use the maintenance configuration:
|
|||||||
| `maintenance.enabled` | Whether the maintenance period is enabled | `true` |
|
| `maintenance.enabled` | Whether the maintenance period is enabled | `true` |
|
||||||
| `maintenance.start` | Time at which the maintenance window starts in `hh:mm` format (e.g. `23:00`) | Required `""` |
|
| `maintenance.start` | Time at which the maintenance window starts in `hh:mm` format (e.g. `23:00`) | Required `""` |
|
||||||
| `maintenance.duration` | Duration of the maintenance window (e.g. `1h`, `30m`) | Required `""` |
|
| `maintenance.duration` | Duration of the maintenance window (e.g. `1h`, `30m`) | Required `""` |
|
||||||
|
| `maintenance.timezone` | Timezone of the maintenance window format (e.g. `Europe/Amsterdam`).<br />See [List of tz database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for more info | `UTC` |
|
||||||
| `maintenance.every` | Days on which the maintenance period applies (e.g. `[Monday, Thursday]`).<br />If left empty, the maintenance window applies every day | `[]` |
|
| `maintenance.every` | Days on which the maintenance period applies (e.g. `[Monday, Thursday]`).<br />If left empty, the maintenance window applies every day | `[]` |
|
||||||
|
|
||||||
> 📝 The maintenance configuration uses UTC
|
|
||||||
|
|
||||||
Here's an example:
|
Here's an example:
|
||||||
```yaml
|
```yaml
|
||||||
maintenance:
|
maintenance:
|
||||||
start: 23:00
|
start: 23:00
|
||||||
duration: 1h
|
duration: 1h
|
||||||
|
timezone: "Europe/Amsterdam"
|
||||||
every: [Monday, Thursday]
|
every: [Monday, Thursday]
|
||||||
```
|
```
|
||||||
Note that you can also specify each day on separate lines:
|
Note that you can also specify each day on separate lines:
|
||||||
@ -1476,6 +1626,7 @@ Note that you can also specify each day on separate lines:
|
|||||||
maintenance:
|
maintenance:
|
||||||
start: 23:00
|
start: 23:00
|
||||||
duration: 1h
|
duration: 1h
|
||||||
|
timezone: "Europe/Amsterdam"
|
||||||
every:
|
every:
|
||||||
- Monday
|
- Monday
|
||||||
- Thursday
|
- Thursday
|
||||||
@ -1646,11 +1797,12 @@ Please refer to Helm's [documentation](https://helm.sh/docs/) to get started.
|
|||||||
Once Helm is set up properly, add the repository as follows:
|
Once Helm is set up properly, add the repository as follows:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
helm repo add minicloudlabs https://minicloudlabs.github.io/helm-charts
|
helm repo add twin https://twin.github.io/helm-charts
|
||||||
|
helm repo update
|
||||||
|
helm install gatus twin/gatus
|
||||||
```
|
```
|
||||||
|
|
||||||
To get more details, please check [chart's configuration](https://github.com/minicloudlabs/helm-charts/tree/main/charts/gatus#configuration)
|
To get more details, please check [chart's configuration](https://github.com/TwiN/helm-charts/blob/master/charts/gatus/README.md).
|
||||||
and [helm file example](https://github.com/minicloudlabs/helm-charts/tree/main/charts/gatus#helmfileyaml-example)
|
|
||||||
|
|
||||||
|
|
||||||
### Terraform
|
### Terraform
|
||||||
@ -2123,7 +2275,7 @@ The path to generate a badge is the following:
|
|||||||
/api/v1/endpoints/{key}/uptimes/{duration}/badge.svg
|
/api/v1/endpoints/{key}/uptimes/{duration}/badge.svg
|
||||||
```
|
```
|
||||||
Where:
|
Where:
|
||||||
- `{duration}` is `7d`, `24h` or `1h`
|
- `{duration}` is `30d` (alpha), `7d`, `24h` or `1h`
|
||||||
- `{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 `-`.
|
||||||
|
|
||||||
For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`,
|
For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`,
|
||||||
@ -2188,7 +2340,7 @@ The endpoint to generate a badge is the following:
|
|||||||
/api/v1/endpoints/{key}/response-times/{duration}/badge.svg
|
/api/v1/endpoints/{key}/response-times/{duration}/badge.svg
|
||||||
```
|
```
|
||||||
Where:
|
Where:
|
||||||
- `{duration}` is `7d`, `24h` or `1h`
|
- `{duration}` is `30d` (alpha), `7d`, `24h` or `1h`
|
||||||
- `{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 `-`.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package alert
|
package alert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -10,7 +13,7 @@ var (
|
|||||||
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
|
ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Alert is a core.Endpoint's alert configuration
|
// Alert is a endpoint.Endpoint's alert configuration
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
// Type of alert (required)
|
// Type of alert (required)
|
||||||
Type Type `yaml:"type"`
|
Type Type `yaml:"type"`
|
||||||
@ -26,6 +29,9 @@ type Alert struct {
|
|||||||
// FailureThreshold is the number of failures in a row needed before triggering the alert
|
// FailureThreshold is the number of failures in a row needed before triggering the alert
|
||||||
FailureThreshold int `yaml:"failure-threshold"`
|
FailureThreshold int `yaml:"failure-threshold"`
|
||||||
|
|
||||||
|
// SuccessThreshold defines how many successful executions must happen in a row before an ongoing incident is marked as resolved
|
||||||
|
SuccessThreshold int `yaml:"success-threshold"`
|
||||||
|
|
||||||
// Description of the alert. Will be included in the alert sent.
|
// Description of the alert. Will be included in the alert sent.
|
||||||
//
|
//
|
||||||
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
|
||||||
@ -38,9 +44,6 @@ type Alert struct {
|
|||||||
// or not for provider.ParseWithDefaultAlert to work. Use Alert.IsSendingOnResolved() for a non-pointer
|
// or not for provider.ParseWithDefaultAlert to work. Use Alert.IsSendingOnResolved() for a non-pointer
|
||||||
SendOnResolved *bool `yaml:"send-on-resolved"`
|
SendOnResolved *bool `yaml:"send-on-resolved"`
|
||||||
|
|
||||||
// SuccessThreshold defines how many successful executions must happen in a row before an ongoing incident is marked as resolved
|
|
||||||
SuccessThreshold int `yaml:"success-threshold"`
|
|
||||||
|
|
||||||
// ResolveKey is an optional field that is used by some providers (i.e. PagerDuty's dedup_key) to resolve
|
// ResolveKey is an optional field that is used by some providers (i.e. PagerDuty's dedup_key) to resolve
|
||||||
// ongoing/triggered incidents
|
// ongoing/triggered incidents
|
||||||
ResolveKey string `yaml:"-"`
|
ResolveKey string `yaml:"-"`
|
||||||
@ -94,3 +97,17 @@ func (alert *Alert) IsSendingOnResolved() bool {
|
|||||||
}
|
}
|
||||||
return *alert.SendOnResolved
|
return *alert.SendOnResolved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checksum returns a checksum of the alert
|
||||||
|
// Used to determine which persisted triggered alert should be deleted on application start
|
||||||
|
func (alert *Alert) Checksum() string {
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write([]byte(string(alert.Type) + "_" +
|
||||||
|
strconv.FormatBool(alert.IsEnabled()) + "_" +
|
||||||
|
strconv.FormatBool(alert.IsSendingOnResolved()) + "_" +
|
||||||
|
strconv.Itoa(alert.SuccessThreshold) + "_" +
|
||||||
|
strconv.Itoa(alert.FailureThreshold) + "_" +
|
||||||
|
alert.GetDescription()),
|
||||||
|
)
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
@ -84,3 +84,109 @@ func TestAlert_IsSendingOnResolved(t *testing.T) {
|
|||||||
t.Error("alert.IsSendingOnResolved() should've returned true, because SendOnResolved was set to true")
|
t.Error("alert.IsSendingOnResolved() should've returned true, because SendOnResolved was set to true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlert_Checksum(t *testing.T) {
|
||||||
|
description1, description2 := "a", "b"
|
||||||
|
yes, no := true, false
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
alert Alert
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "barebone",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
},
|
||||||
|
expected: "fed0580e44ed5701dbba73afa1f14b2c53ca5a7b8067a860441c212916057fe3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-1",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Description: &description1,
|
||||||
|
},
|
||||||
|
expected: "005f407ebe506e74a4aeb46f74c28b376debead7011e1b085da3840f72ba9707",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "3c2c4a9570cdc614006993c21f79a860a7f5afea10cf70d1a79d3c49342ef2c8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-enabled-false",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Enabled: &no,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "837945c2b4cd5e961db3e63e10c348d4f1c3446ba68cf5a48e35a1ae22cf0c22",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-enabled-true",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Enabled: &yes, // it defaults to true if not set, but just to make sure
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "3c2c4a9570cdc614006993c21f79a860a7f5afea10cf70d1a79d3c49342ef2c8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-enabled-true-and-send-on-resolved-true",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeDiscord,
|
||||||
|
Enabled: &yes,
|
||||||
|
SendOnResolved: &yes,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "bf1436995a880eb4a352c74c5dfee1f1b5ff6b9fc55aef9bf411b3631adfd80c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-failure-threshold-7",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
FailureThreshold: 7,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "8bd479e18bda393d4c924f5a0d962e825002168dedaa88b445e435db7bacffd3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-failure-threshold-9",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
FailureThreshold: 9,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "5abdfce5236e344996d264d526e769c07cb0d3d329a999769a1ff84b157ca6f1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-success-threshold-5",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
SuccessThreshold: 7,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "c0000e73626b80e212cfc24830de7094568f648e37f3e16f9e68c7f8ef75c34c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with-description-2-and-success-threshold-1",
|
||||||
|
alert: Alert{
|
||||||
|
Type: TypeSlack,
|
||||||
|
SuccessThreshold: 1,
|
||||||
|
Description: &description2,
|
||||||
|
},
|
||||||
|
expected: "5c28963b3a76104cfa4a0d79c89dd29ec596c8cfa4b1af210ec83d6d41587b5f",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
scenario.alert.ValidateAndSetDefaults()
|
||||||
|
if checksum := scenario.alert.Checksum(); checksum != scenario.expected {
|
||||||
|
t.Errorf("expected checksum %v, got %v", scenario.expected, checksum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,6 +23,9 @@ const (
|
|||||||
// TypeGitLab is the Type for the gitlab alerting provider
|
// TypeGitLab is the Type for the gitlab alerting provider
|
||||||
TypeGitLab Type = "gitlab"
|
TypeGitLab Type = "gitlab"
|
||||||
|
|
||||||
|
// TypeGitea is the Type for the gitea alerting provider
|
||||||
|
TypeGitea Type = "gitea"
|
||||||
|
|
||||||
// TypeGoogleChat is the Type for the googlechat alerting provider
|
// TypeGoogleChat is the Type for the googlechat alerting provider
|
||||||
TypeGoogleChat Type = "googlechat"
|
TypeGoogleChat Type = "googlechat"
|
||||||
|
|
||||||
@ -64,4 +67,7 @@ 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"
|
||||||
|
|
||||||
|
// TypeZulip is the Type for the Zulip alerting provider
|
||||||
|
TypeZulip Type = "zulip"
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/gitea"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||||
@ -25,8 +26,10 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/zulip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the configuration for alerting providers
|
// Config is the configuration for alerting providers
|
||||||
@ -49,6 +52,9 @@ type Config struct {
|
|||||||
// GitLab is the configuration for the gitlab alerting provider
|
// GitLab is the configuration for the gitlab alerting provider
|
||||||
GitLab *gitlab.AlertProvider `yaml:"gitlab,omitempty"`
|
GitLab *gitlab.AlertProvider `yaml:"gitlab,omitempty"`
|
||||||
|
|
||||||
|
// Gitea is the configuration for the gitea alerting provider
|
||||||
|
Gitea *gitea.AlertProvider `yaml:"gitea,omitempty"`
|
||||||
|
|
||||||
// GoogleChat is the configuration for the googlechat alerting provider
|
// GoogleChat is the configuration for the googlechat alerting provider
|
||||||
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
|
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
|
||||||
|
|
||||||
@ -85,11 +91,17 @@ type Config struct {
|
|||||||
// Teams is the configuration for the teams alerting provider
|
// Teams is the configuration for the teams alerting provider
|
||||||
Teams *teams.AlertProvider `yaml:"teams,omitempty"`
|
Teams *teams.AlertProvider `yaml:"teams,omitempty"`
|
||||||
|
|
||||||
|
// TeamsWorkflows is the configuration for the teams alerting provider using the new Workflow App Webhook Connector
|
||||||
|
TeamsWorkflows *teamsworkflows.AlertProvider `yaml:"teams-workflows,omitempty"`
|
||||||
|
|
||||||
// Telegram is the configuration for the telegram alerting provider
|
// Telegram is the configuration for the telegram alerting provider
|
||||||
Telegram *telegram.AlertProvider `yaml:"telegram,omitempty"`
|
Telegram *telegram.AlertProvider `yaml:"telegram,omitempty"`
|
||||||
|
|
||||||
// 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"`
|
||||||
|
|
||||||
|
// Zulip is the configuration for the zulip alerting provider
|
||||||
|
Zulip *zulip.AlertProvider `yaml:"zulip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
|
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
@ -57,14 +57,14 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
sess, err := provider.createSession()
|
sess, err := provider.createSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
svc := ses.New(sess)
|
svc := ses.New(sess)
|
||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||||
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
|
emails := strings.Split(provider.getToForGroup(ep.Group), ",")
|
||||||
|
|
||||||
input := &ses.SendEmailInput{
|
input := &ses.SendEmailInput{
|
||||||
Destination: &ses.Destination{
|
Destination: &ses.Destination{
|
||||||
@ -110,14 +110,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildMessageSubjectAndBody builds the message subject and body
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
@ -95,10 +95,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
|
||||||
@ -50,16 +50,18 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
|
func (provider *AlertProvider) buildHTTPRequest(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) *http.Request {
|
||||||
body, url, method := provider.Body, provider.URL, provider.Method
|
body, url, method := provider.Body, provider.URL, provider.Method
|
||||||
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
|
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
|
url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
|
body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
|
url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
|
||||||
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
|
body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
|
||||||
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
|
url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
|
||||||
|
body = strings.ReplaceAll(body, "[RESULT_ERRORS]", strings.Join(result.Errors, ","))
|
||||||
|
url = strings.ReplaceAll(url, "[RESULT_ERRORS]", strings.Join(result.Errors, ","))
|
||||||
if resolved {
|
if resolved {
|
||||||
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
|
||||||
@ -78,8 +80,8 @@ func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
request := provider.buildHTTPRequest(endpoint, alert, resolved)
|
request := provider.buildHTTPRequest(ep, alert, result, 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
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -90,10 +90,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -138,8 +138,55 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
request := customAlertProvider.buildHTTPRequest(
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
||||||
&alert.Alert{Description: &alertDescription},
|
&alert.Alert{Description: &alertDescription},
|
||||||
|
&endpoint.Result{Errors: []string{}},
|
||||||
|
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 TestAlertProviderWithResultErrors_buildHTTPRequest(t *testing.T) {
|
||||||
|
customAlertWithErrorsProvider := &AlertProvider{
|
||||||
|
URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]&error=[RESULT_ERRORS]",
|
||||||
|
Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED],[RESULT_ERRORS]",
|
||||||
|
}
|
||||||
|
alertDescription := "alert-description"
|
||||||
|
scenarios := []struct {
|
||||||
|
AlertProvider *AlertProvider
|
||||||
|
Resolved bool
|
||||||
|
ExpectedURL string
|
||||||
|
ExpectedBody string
|
||||||
|
Errors []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
AlertProvider: customAlertWithErrorsProvider,
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=RESOLVED&description=alert-description&url=https://example.com&error=",
|
||||||
|
ExpectedBody: "endpoint-name,endpoint-group,alert-description,https://example.com,RESOLVED,",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AlertProvider: customAlertWithErrorsProvider,
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=TRIGGERED&description=alert-description&url=https://example.com&error=error1,error2",
|
||||||
|
ExpectedBody: "endpoint-name,endpoint-group,alert-description,https://example.com,TRIGGERED,error1,error2",
|
||||||
|
Errors: []string{"error1", "error2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders-and-result-errors", scenario.Resolved), func(t *testing.T) {
|
||||||
|
request := customAlertWithErrorsProvider.buildHTTPRequest(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"},
|
||||||
|
&alert.Alert{Description: &alertDescription},
|
||||||
|
&endpoint.Result{Errors: scenario.Errors},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if request.URL.String() != scenario.ExpectedURL {
|
if request.URL.String() != scenario.ExpectedURL {
|
||||||
@ -188,8 +235,9 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) {
|
||||||
request := customAlertProvider.buildHTTPRequest(
|
request := customAlertProvider.buildHTTPRequest(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&alert.Alert{Description: &alertDescription},
|
&alert.Alert{Description: &alertDescription},
|
||||||
|
&endpoint.Result{},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if request.URL.String() != scenario.ExpectedURL {
|
if request.URL.String() != scenario.ExpectedURL {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Discord
|
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||||
@ -47,9 +47,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -85,14 +85,14 @@ type Field struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
var colorCode int
|
var colorCode int
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
colorCode = 3066993
|
colorCode = 3066993
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
colorCode = 15158332
|
colorCode = 15158332
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -127,10 +127,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -191,18 +191,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: conditionResults,
|
ConditionResults: conditionResults,
|
||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
gomail "gopkg.in/mail.v2"
|
gomail "gopkg.in/mail.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,17 +53,17 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
var username string
|
var username string
|
||||||
if len(provider.Username) > 0 {
|
if len(provider.Username) > 0 {
|
||||||
username = provider.Username
|
username = provider.Username
|
||||||
} else {
|
} else {
|
||||||
username = provider.From
|
username = provider.From
|
||||||
}
|
}
|
||||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", provider.From)
|
m.SetHeader("From", provider.From)
|
||||||
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
|
m.SetHeader("To", strings.Split(provider.getToForGroup(ep.Group), ",")...)
|
||||||
m.SetHeader("Subject", subject)
|
m.SetHeader("Subject", subject)
|
||||||
m.SetBody("text/plain", body)
|
m.SetBody("text/plain", body)
|
||||||
var d *gomail.Dialer
|
var d *gomail.Dialer
|
||||||
@ -87,14 +87,14 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildMessageSubjectAndBody builds the message subject and body
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
@ -97,10 +97,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
167
alerting/provider/gitea/gitea.go
Normal file
167
alerting/provider/gitea/gitea.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using Discord
|
||||||
|
type AlertProvider struct {
|
||||||
|
RepositoryURL string `yaml:"repository-url"` // The URL of the Gitea repository to create issues in
|
||||||
|
Token string `yaml:"token"` // Token requires at least RW on issues and RO on metadata
|
||||||
|
|
||||||
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||||
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
|
||||||
|
// Assignees is a list of users to assign the issue to
|
||||||
|
Assignees []string `yaml:"assignees,omitempty"`
|
||||||
|
|
||||||
|
username string
|
||||||
|
repositoryOwner string
|
||||||
|
repositoryName string
|
||||||
|
giteaClient *gitea.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
if provider.ClientConfig == nil {
|
||||||
|
provider.ClientConfig = client.GetDefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(provider.Token) == 0 || len(provider.RepositoryURL) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Validate format of the repository URL
|
||||||
|
repositoryURL, err := url.Parse(provider.RepositoryURL)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
baseURL := repositoryURL.Scheme + "://" + repositoryURL.Host
|
||||||
|
pathParts := strings.Split(repositoryURL.Path, "/")
|
||||||
|
if len(pathParts) != 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
provider.repositoryOwner = pathParts[1]
|
||||||
|
provider.repositoryName = pathParts[2]
|
||||||
|
|
||||||
|
opts := []gitea.ClientOption{
|
||||||
|
gitea.SetToken(provider.Token),
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider.ClientConfig != nil && provider.ClientConfig.Insecure {
|
||||||
|
// add new http client for skip verify
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
opts = append(opts, gitea.SetHTTPClient(httpClient))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.giteaClient, err = gitea.NewClient(baseURL, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _, err := provider.giteaClient.GetMyUserInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.username = user.UserName
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
|
title := "alert(gatus): " + ep.DisplayName()
|
||||||
|
if !resolved {
|
||||||
|
_, _, err := provider.giteaClient.CreateIssue(
|
||||||
|
provider.repositoryOwner,
|
||||||
|
provider.repositoryName,
|
||||||
|
gitea.CreateIssueOption{
|
||||||
|
Title: title,
|
||||||
|
Body: provider.buildIssueBody(ep, alert, result),
|
||||||
|
Assignees: provider.Assignees,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create issue: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, _, err := provider.giteaClient.ListRepoIssues(
|
||||||
|
provider.repositoryOwner,
|
||||||
|
provider.repositoryName,
|
||||||
|
gitea.ListIssueOption{
|
||||||
|
State: gitea.StateOpen,
|
||||||
|
CreatedBy: provider.username,
|
||||||
|
ListOptions: gitea.ListOptions{
|
||||||
|
Page: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list issues: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, issue := range issues {
|
||||||
|
if issue.Title == title {
|
||||||
|
stateClosed := gitea.StateClosed
|
||||||
|
_, _, err = provider.giteaClient.EditIssue(
|
||||||
|
provider.repositoryOwner,
|
||||||
|
provider.repositoryName,
|
||||||
|
issue.ID,
|
||||||
|
gitea.EditIssueOption{
|
||||||
|
State: &stateClosed,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close issue: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildIssueBody builds the body of the issue
|
||||||
|
func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result) string {
|
||||||
|
var formattedConditionResults string
|
||||||
|
if len(result.ConditionResults) > 0 {
|
||||||
|
formattedConditionResults = "\n\n## Condition results\n"
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
var prefix string
|
||||||
|
if conditionResult.Success {
|
||||||
|
prefix = ":white_check_mark:"
|
||||||
|
} else {
|
||||||
|
prefix = ":x:"
|
||||||
|
}
|
||||||
|
formattedConditionResults += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var description string
|
||||||
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
|
description = ":\n> " + alertDescription
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
|
return message + description + formattedConditionResults
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
169
alerting/provider/gitea/gitea_test.go
Normal file
169
alerting/provider/gitea/gitea_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package gitea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "invalid",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "", Token: ""},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid-token",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "https://gitea.com/TwiN/test", Token: "12345"},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "missing-repository-name",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "https://gitea.com/TwiN", Token: "12345"},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "enterprise-client",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "https://gitea.example.com/TwiN/test", Token: "12345"},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "invalid-url",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "gitea.com/TwiN/test", Token: "12345"},
|
||||||
|
Expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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_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-error",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "https://gitea.com/TwiN/test", Token: "12345"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-error",
|
||||||
|
Provider: AlertProvider{RepositoryURL: "https://gitea.com/TwiN/test", Token: "12345"},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
scenario.Provider.giteaClient, _ = gitea.NewClient("https://gitea.com")
|
||||||
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
|
err := scenario.Provider.Send(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if scenario.ExpectedError && err == nil {
|
||||||
|
t.Error("expected error, got none")
|
||||||
|
}
|
||||||
|
if !scenario.ExpectedError && err != nil {
|
||||||
|
t.Error("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
|
firstDescription := "description-1"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Endpoint endpoint.Endpoint
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
|
ExpectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-no-description",
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
|
||||||
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body := scenario.Provider.buildIssueBody(
|
||||||
|
&scenario.Endpoint,
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
|
)
|
||||||
|
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
||||||
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
|
t.Error("expected default alert to be not nil")
|
||||||
|
}
|
||||||
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
|
t.Error("expected default alert to be nil")
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/google/go-github/v48/github"
|
"github.com/google/go-github/v48/github"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
@ -70,12 +70,12 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
title := "alert(gatus): " + endpoint.DisplayName()
|
title := "alert(gatus): " + ep.DisplayName()
|
||||||
if !resolved {
|
if !resolved {
|
||||||
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
_, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
|
||||||
Title: github.String(title),
|
Title: github.String(title),
|
||||||
Body: github.String(provider.buildIssueBody(endpoint, alert, result)),
|
Body: github.String(provider.buildIssueBody(ep, alert, result)),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create issue: %w", err)
|
return fmt.Errorf("failed to create issue: %w", err)
|
||||||
@ -104,7 +104,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildIssueBody builds the body of the issue
|
// buildIssueBody builds the body of the issue
|
||||||
func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string {
|
func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result) string {
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
formattedConditionResults = "\n\n## Condition results\n"
|
formattedConditionResults = "\n\n## Condition results\n"
|
||||||
@ -122,7 +122,7 @@ func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *al
|
|||||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
description = ":\n> " + alertDescription
|
description = ":\n> " + alertDescription
|
||||||
}
|
}
|
||||||
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
return message + description + formattedConditionResults
|
return message + description + formattedConditionResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
"github.com/google/go-github/v48/github"
|
"github.com/google/go-github/v48/github"
|
||||||
)
|
)
|
||||||
@ -85,10 +85,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
scenario.Provider.githubClient = github.NewClient(nil)
|
scenario.Provider.githubClient = github.NewClient(nil)
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -109,7 +109,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
NoConditions bool
|
NoConditions bool
|
||||||
@ -117,14 +117,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "triggered-with-no-description",
|
Name: "triggered-with-no-description",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{FailureThreshold: 10},
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`",
|
||||||
@ -132,7 +132,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "triggered-with-no-conditions",
|
Name: "triggered-with-no-conditions",
|
||||||
NoConditions: true,
|
NoConditions: true,
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10},
|
||||||
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
|
ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1",
|
||||||
@ -140,9 +140,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: true},
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
{Condition: "[STATUS] == 200", Success: false},
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
}
|
}
|
||||||
@ -150,7 +150,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildIssueBody(
|
body := scenario.Provider.buildIssueBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{ConditionResults: conditionResults},
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
)
|
)
|
||||||
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
|
||||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,11 +51,11 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
if len(alert.ResolveKey) == 0 {
|
if len(alert.ResolveKey) == 0 {
|
||||||
alert.ResolveKey = uuid.NewString()
|
alert.ResolveKey = uuid.NewString()
|
||||||
}
|
}
|
||||||
buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildAlertBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -94,21 +94,21 @@ func (provider *AlertProvider) monitoringTool() string {
|
|||||||
return "gatus"
|
return "gatus"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) service(endpoint *core.Endpoint) string {
|
func (provider *AlertProvider) service(ep *endpoint.Endpoint) string {
|
||||||
if len(provider.Service) > 0 {
|
if len(provider.Service) > 0 {
|
||||||
return provider.Service
|
return provider.Service
|
||||||
}
|
}
|
||||||
return endpoint.DisplayName()
|
return ep.DisplayName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildAlertBody builds the body of the alert
|
// buildAlertBody builds the body of the alert
|
||||||
func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildAlertBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body := AlertBody{
|
body := AlertBody{
|
||||||
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(endpoint)),
|
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(ep)),
|
||||||
StartTime: result.Timestamp.Format(time.RFC3339),
|
StartTime: result.Timestamp.Format(time.RFC3339),
|
||||||
Service: provider.service(endpoint),
|
Service: provider.service(ep),
|
||||||
MonitoringTool: provider.monitoringTool(),
|
MonitoringTool: provider.monitoringTool(),
|
||||||
Hosts: endpoint.URL,
|
Hosts: ep.URL,
|
||||||
GitlabEnvironmentName: provider.EnvironmentName,
|
GitlabEnvironmentName: provider.EnvironmentName,
|
||||||
Severity: provider.Severity,
|
Severity: provider.Severity,
|
||||||
Fingerprint: alert.ResolveKey,
|
Fingerprint: alert.ResolveKey,
|
||||||
@ -135,9 +135,9 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
|
|||||||
}
|
}
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
body.Description = message + description + formattedConditionResults
|
body.Description = message + description + formattedConditionResults
|
||||||
bodyAsJSON, _ := json.Marshal(body)
|
bodyAsJSON, _ := json.Marshal(body)
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -84,10 +84,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -108,21 +108,21 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
|||||||
firstDescription := "description-1"
|
firstDescription := "description-1"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no-description",
|
Name: "no-description",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{FailureThreshold: 10},
|
Alert: alert.Alert{FailureThreshold: 10},
|
||||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||||
@ -133,8 +133,8 @@ func TestAlertProvider_buildAlertBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildAlertBody(
|
body := scenario.Provider.buildAlertBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: true},
|
{Condition: "[CONNECTED] == true", Success: true},
|
||||||
{Condition: "[STATUS] == 200", Success: false},
|
{Condition: "[STATUS] == 200", Success: false},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
// AlertProvider is the configuration necessary for sending an alert using Google chat
|
||||||
@ -50,9 +50,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -112,7 +112,7 @@ type OpenLink struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
@ -143,7 +143,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
Widgets: []Widgets{
|
Widgets: []Widgets{
|
||||||
{
|
{
|
||||||
KeyValue: &KeyValue{
|
KeyValue: &KeyValue{
|
||||||
TopLabel: endpoint.DisplayName(),
|
TopLabel: ep.DisplayName(),
|
||||||
Content: message,
|
Content: message,
|
||||||
ContentMultiline: "true",
|
ContentMultiline: "true",
|
||||||
BottomLabel: description,
|
BottomLabel: description,
|
||||||
@ -166,7 +166,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if endpoint.Type() == core.EndpointTypeHTTP {
|
if ep.Type() == endpoint.TypeHTTP {
|
||||||
// We only include a button targeting the URL if the endpoint is an HTTP endpoint
|
// We only include a button targeting the URL if the endpoint is an HTTP endpoint
|
||||||
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
|
// If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways.
|
||||||
// See https://github.com/TwiN/gatus/issues/362
|
// See https://github.com/TwiN/gatus/issues/362
|
||||||
@ -175,7 +175,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
{
|
{
|
||||||
TextButton: TextButton{
|
TextButton: TextButton{
|
||||||
Text: "URL",
|
Text: "URL",
|
||||||
OnClick: OnClick{OpenLink: OpenLink{URL: endpoint.URL}},
|
OnClick: OnClick{OpenLink: OpenLink{URL: ep.URL}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
&endpoint.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -141,7 +141,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
secondDescription := "description-2"
|
secondDescription := "description-2"
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
Resolved bool
|
Resolved bool
|
||||||
@ -149,7 +149,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
@ -157,7 +157,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
@ -165,7 +165,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
Name: "icmp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "icmp://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
@ -173,7 +173,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
Name: "tcp-should-not-include-url", // See https://github.com/TwiN/gatus/issues/362
|
||||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
Endpoint: endpoint.Endpoint{Name: "endpoint-name", URL: "tcp://example.org"},
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
@ -185,8 +185,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultPriority = 5
|
const DefaultPriority = 5
|
||||||
@ -41,8 +41,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -67,12 +67,12 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
@ -88,7 +88,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
message += " with the following description: " + alert.GetDescription()
|
message += " with the following description: " + alert.GetDescription()
|
||||||
}
|
}
|
||||||
message += formattedConditionResults
|
message += formattedConditionResults
|
||||||
title := "Gatus: " + endpoint.DisplayName()
|
title := "Gatus: " + ep.DisplayName()
|
||||||
if provider.Title != "" {
|
if provider.Title != "" {
|
||||||
title = provider.Title
|
title = provider.Title
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
@ -49,7 +49,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
var (
|
var (
|
||||||
description = "custom-description"
|
description = "custom-description"
|
||||||
//title = "custom-title"
|
//title = "custom-title"
|
||||||
endpoint = "custom-endpoint"
|
endpointName = "custom-endpoint"
|
||||||
)
|
)
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
@ -63,30 +63,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
|
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpointName, description),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
|
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpointName, description),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "custom-title",
|
Name: "custom-title",
|
||||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
||||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpoint, description),
|
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpointName, description),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: endpoint},
|
&endpoint.Endpoint{Name: endpointName},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
|
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
|
||||||
@ -46,8 +46,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
|
url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
|
||||||
request, err := http.NewRequest(http.MethodPost, url, buffer)
|
request, err := http.NewRequest(http.MethodPost, url, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -103,9 +103,9 @@ type Icon struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body := Body{
|
body := Body{
|
||||||
Channel: "id:" + provider.getChannelIDForGroup(endpoint.Group),
|
Channel: "id:" + provider.getChannelIDForGroup(ep.Group),
|
||||||
Content: Content{
|
Content: Content{
|
||||||
ClassName: "ChatMessage.Block",
|
ClassName: "ChatMessage.Block",
|
||||||
Sections: []Section{{
|
Sections: []Section{{
|
||||||
@ -116,10 +116,10 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
}
|
}
|
||||||
if resolved {
|
if resolved {
|
||||||
body.Content.Style = "SUCCESS"
|
body.Content.Style = "SUCCESS"
|
||||||
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
body.Content.Style = "WARNING"
|
body.Content.Style = "WARNING"
|
||||||
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
icon := "warning"
|
icon := "warning"
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -146,7 +146,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
Resolved bool
|
Resolved bool
|
||||||
ExpectedBody string
|
ExpectedBody string
|
||||||
@ -154,7 +154,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
||||||
@ -162,7 +162,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "triggered-with-group",
|
Name: "triggered-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"WARNING","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"warning"},"style":"WARNING"},"style":"WARNING","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row"}]}}`,
|
||||||
@ -170,7 +170,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
||||||
@ -178,7 +178,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "resolved-with-group",
|
Name: "resolved-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
ExpectedBody: `{"channel":"id:","content":{"className":"ChatMessage.Block","style":"SUCCESS","sections":[{"className":"MessageSection","elements":[{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[CONNECTED] == true"},{"className":"MessageText","accessory":{"className":"MessageIcon","icon":{"icon":"success"},"style":"SUCCESS"},"style":"SUCCESS","size":"REGULAR","content":"[STATUS] == 200"}],"header":"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row"}]}}`,
|
||||||
@ -189,8 +189,8 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Matrix
|
// AlertProvider is the configuration necessary for sending an alert using Matrix
|
||||||
@ -61,9 +61,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
config := provider.getConfigForGroup(endpoint.Group)
|
config := provider.getConfigForGroup(ep.Group)
|
||||||
if config.ServerURL == "" {
|
if config.ServerURL == "" {
|
||||||
config.ServerURL = defaultServerURL
|
config.ServerURL = defaultServerURL
|
||||||
}
|
}
|
||||||
@ -103,23 +103,23 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
MsgType: "m.text",
|
MsgType: "m.text",
|
||||||
Format: "org.matrix.custom.html",
|
Format: "org.matrix.custom.html",
|
||||||
Body: buildPlaintextMessageBody(endpoint, alert, result, resolved),
|
Body: buildPlaintextMessageBody(ep, alert, result, resolved),
|
||||||
FormattedBody: buildHTMLMessageBody(endpoint, alert, result, resolved),
|
FormattedBody: buildHTMLMessageBody(ep, alert, result, resolved),
|
||||||
})
|
})
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
// buildPlaintextMessageBody builds the message body in plaintext to include in request
|
||||||
func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func buildPlaintextMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
@ -139,12 +139,12 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildHTMLMessageBody builds the message body in HTML to include in request
|
// buildHTMLMessageBody builds the message body in HTML to include in request
|
||||||
func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func buildHTMLMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -149,10 +149,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -197,10 +197,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -9,13 +9,16 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
// AlertProvider is the configuration necessary for sending an alert using Mattermost
|
||||||
type AlertProvider struct {
|
type AlertProvider struct {
|
||||||
WebhookURL string `yaml:"webhook-url"`
|
WebhookURL string `yaml:"webhook-url"`
|
||||||
|
|
||||||
|
// Channel is the optional setting to override the default webhook's channel
|
||||||
|
Channel string `yaml:"channel,omitempty"`
|
||||||
|
|
||||||
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
|
||||||
@ -50,9 +53,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -70,6 +73,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Body struct {
|
type Body struct {
|
||||||
|
Channel string `json:"channel,omitempty"` // Optional channel override
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IconURL string `json:"icon_url"`
|
IconURL string `json:"icon_url"`
|
||||||
@ -92,13 +96,13 @@ type Field struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
@ -118,6 +122,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
description = ":\n> " + alertDescription
|
description = ":\n> " + alertDescription
|
||||||
}
|
}
|
||||||
body := Body{
|
body := Body{
|
||||||
|
Channel: provider.Channel,
|
||||||
Text: "",
|
Text: "",
|
||||||
Username: "gatus",
|
Username: "gatus",
|
||||||
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -120,10 +120,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -168,10 +168,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -33,8 +33,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -60,12 +60,12 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Originator: provider.Originator,
|
Originator: provider.Originator,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -83,10 +83,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -131,10 +131,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,6 +25,10 @@ type AlertProvider struct {
|
|||||||
URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
|
URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
|
||||||
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
|
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
|
||||||
Token string `yaml:"token,omitempty"` // Defaults to ""
|
Token string `yaml:"token,omitempty"` // Defaults to ""
|
||||||
|
Email string `yaml:"email,omitempty"` // Defaults to ""
|
||||||
|
Click string `yaml:"click,omitempty"` // Defaults to ""
|
||||||
|
DisableFirebase bool `yaml:"disable-firebase,omitempty"` // Defaults to false
|
||||||
|
DisableCache bool `yaml:"disable-cache,omitempty"` // Defaults to false
|
||||||
|
|
||||||
// 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"`
|
||||||
@ -46,8 +50,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.URL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -56,6 +60,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
if len(provider.Token) > 0 {
|
if len(provider.Token) > 0 {
|
||||||
request.Header.Set("Authorization", "Bearer "+provider.Token)
|
request.Header.Set("Authorization", "Bearer "+provider.Token)
|
||||||
}
|
}
|
||||||
|
if provider.DisableFirebase {
|
||||||
|
request.Header.Set("Firebase", "no")
|
||||||
|
}
|
||||||
|
if provider.DisableCache {
|
||||||
|
request.Header.Set("Cache", "no")
|
||||||
|
}
|
||||||
response, err := client.GetHTTPClient(nil).Do(request)
|
response, err := client.GetHTTPClient(nil).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -74,10 +84,12 @@ type Body struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Click string `json:"click,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, formattedConditionResults, tag string
|
var message, formattedConditionResults, tag string
|
||||||
if resolved {
|
if resolved {
|
||||||
tag = "white_check_mark"
|
tag = "white_check_mark"
|
||||||
@ -101,10 +113,12 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
message += formattedConditionResults
|
message += formattedConditionResults
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Topic: provider.Topic,
|
Topic: provider.Topic,
|
||||||
Title: "Gatus: " + endpoint.DisplayName(),
|
Title: "Gatus: " + ep.DisplayName(),
|
||||||
Message: message,
|
Message: message,
|
||||||
Tags: []string{tag},
|
Tags: []string{tag},
|
||||||
Priority: provider.Priority,
|
Priority: provider.Priority,
|
||||||
|
Email: provider.Email,
|
||||||
|
Click: provider.Click,
|
||||||
})
|
})
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,13 @@ package ntfy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
@ -88,14 +91,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`,
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-email",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-email",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2, Email: "test@example.com", Click: "example.com"},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2,"email":"test@example.com","click":"example.com"}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -112,3 +129,99 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
|
description := "description-1"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
ExpectedBody string
|
||||||
|
ExpectedHeaders map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com"},
|
||||||
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
|
||||||
|
ExpectedHeaders: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "no firebase",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true},
|
||||||
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
|
||||||
|
ExpectedHeaders: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Firebase": "no",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "no cache",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableCache: true},
|
||||||
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
|
||||||
|
ExpectedHeaders: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache": "no",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "neither firebase & cache",
|
||||||
|
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1, Email: "test@example.com", Click: "example.com", DisableFirebase: true, DisableCache: true},
|
||||||
|
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1,"email":"test@example.com","click":"example.com"}`,
|
||||||
|
ExpectedHeaders: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Firebase": "no",
|
||||||
|
"Cache": "no",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
// Start a local HTTP server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// Test request parameters
|
||||||
|
for header, value := range scenario.ExpectedHeaders {
|
||||||
|
if value != req.Header.Get(header) {
|
||||||
|
t.Errorf("expected: %s, got: %s", value, req.Header.Get(header))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(req.Body)
|
||||||
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
|
}
|
||||||
|
// Send response to be tested
|
||||||
|
rw.Write([]byte(`OK`))
|
||||||
|
}))
|
||||||
|
// Close the server when test finishes
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
scenario.Provider.URL = server.URL
|
||||||
|
err := scenario.Provider.Send(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Encountered an error on Send: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -59,13 +59,13 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
//
|
//
|
||||||
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
// Relevant: https://docs.opsgenie.com/docs/alert-api
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
err := provider.createAlert(endpoint, alert, result, resolved)
|
err := provider.createAlert(ep, alert, result, resolved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resolved {
|
if resolved {
|
||||||
err = provider.closeAlert(endpoint, alert)
|
err = provider.closeAlert(ep, alert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -75,20 +75,20 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
||||||
alert.ResolveKey = ""
|
alert.ResolveKey = ""
|
||||||
} else {
|
} else {
|
||||||
alert.ResolveKey = provider.alias(buildKey(endpoint))
|
alert.ResolveKey = provider.alias(buildKey(ep))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) createAlert(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) createAlert(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
payload := provider.buildCreateRequestBody(endpoint, alert, result, resolved)
|
payload := provider.buildCreateRequestBody(ep, alert, result, resolved)
|
||||||
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
return provider.sendRequest(restAPI, http.MethodPost, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) closeAlert(endpoint *core.Endpoint, alert *alert.Alert) error {
|
func (provider *AlertProvider) closeAlert(ep *endpoint.Endpoint, alert *alert.Alert) error {
|
||||||
payload := provider.buildCloseRequestBody(endpoint, alert)
|
payload := provider.buildCloseRequestBody(ep, alert)
|
||||||
url := restAPI + "/" + provider.alias(buildKey(endpoint)) + "/close?identifierType=alias"
|
url := restAPI + "/" + provider.alias(buildKey(ep)) + "/close?identifierType=alias"
|
||||||
return provider.sendRequest(url, http.MethodPost, payload)
|
return provider.sendRequest(url, http.MethodPost, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,17 +115,17 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) alertCreateRequest {
|
func (provider *AlertProvider) buildCreateRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) alertCreateRequest {
|
||||||
var message, description string
|
var message, description string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription())
|
||||||
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("%s - %s", endpoint.Name, alert.GetDescription())
|
message = fmt.Sprintf("%s - %s", ep.Name, alert.GetDescription())
|
||||||
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
if endpoint.Group != "" {
|
if ep.Group != "" {
|
||||||
message = fmt.Sprintf("[%s] %s", endpoint.Group, message)
|
message = fmt.Sprintf("[%s] %s", ep.Group, message)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
for _, conditionResult := range result.ConditionResults {
|
for _, conditionResult := range result.ConditionResults {
|
||||||
@ -138,10 +138,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
|
|||||||
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
|
||||||
}
|
}
|
||||||
description = description + "\n" + formattedConditionResults
|
description = description + "\n" + formattedConditionResults
|
||||||
key := buildKey(endpoint)
|
key := buildKey(ep)
|
||||||
details := map[string]string{
|
details := map[string]string{
|
||||||
"endpoint:url": endpoint.URL,
|
"endpoint:url": ep.URL,
|
||||||
"endpoint:group": endpoint.Group,
|
"endpoint:group": ep.Group,
|
||||||
"result:hostname": result.Hostname,
|
"result:hostname": result.Hostname,
|
||||||
"result:ip": result.IP,
|
"result:ip": result.IP,
|
||||||
"result:dns_code": result.DNSRCode,
|
"result:dns_code": result.DNSRCode,
|
||||||
@ -167,10 +167,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *AlertProvider) buildCloseRequestBody(endpoint *core.Endpoint, alert *alert.Alert) alertCloseRequest {
|
func (provider *AlertProvider) buildCloseRequestBody(ep *endpoint.Endpoint, alert *alert.Alert) alertCloseRequest {
|
||||||
return alertCloseRequest{
|
return alertCloseRequest{
|
||||||
Source: buildKey(endpoint),
|
Source: buildKey(ep),
|
||||||
Note: fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()),
|
Note: fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,12 +211,12 @@ func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
|||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKey(endpoint *core.Endpoint) string {
|
func buildKey(ep *endpoint.Endpoint) string {
|
||||||
name := toKebabCase(endpoint.Name)
|
name := toKebabCase(ep.Name)
|
||||||
if endpoint.Group == "" {
|
if ep.Group == "" {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
return toKebabCase(endpoint.Group) + "-" + name
|
return toKebabCase(ep.Group) + "-" + name
|
||||||
}
|
}
|
||||||
|
|
||||||
func toKebabCase(val string) string {
|
func toKebabCase(val string) string {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,10 +79,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -106,8 +106,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider *AlertProvider
|
Provider *AlertProvider
|
||||||
Alert *alert.Alert
|
Alert *alert.Alert
|
||||||
Endpoint *core.Endpoint
|
Endpoint *endpoint.Endpoint
|
||||||
Result *core.Result
|
Result *endpoint.Result
|
||||||
Resolved bool
|
Resolved bool
|
||||||
want alertCreateRequest
|
want alertCreateRequest
|
||||||
}{
|
}{
|
||||||
@ -115,8 +115,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Name: "missing all params (unresolved)",
|
Name: "missing all params (unresolved)",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
Result: &core.Result{},
|
Result: &endpoint.Result{},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
want: alertCreateRequest{
|
want: alertCreateRequest{
|
||||||
Message: " - ",
|
Message: " - ",
|
||||||
@ -133,8 +133,8 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Name: "missing all params (resolved)",
|
Name: "missing all params (resolved)",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
Result: &core.Result{},
|
Result: &endpoint.Result{},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
want: alertCreateRequest{
|
want: alertCreateRequest{
|
||||||
Message: "RESOLVED: - ",
|
Message: "RESOLVED: - ",
|
||||||
@ -154,11 +154,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Description: &description,
|
Description: &description,
|
||||||
FailureThreshold: 3,
|
FailureThreshold: 3,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my super app",
|
Name: "my super app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@ -194,11 +194,11 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Description: &description,
|
Description: &description,
|
||||||
SuccessThreshold: 4,
|
SuccessThreshold: 4,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my mega app",
|
Name: "my mega app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@ -226,17 +226,17 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
|
|||||||
Description: &description,
|
Description: &description,
|
||||||
FailureThreshold: 6,
|
FailureThreshold: 6,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "my app",
|
Name: "my app",
|
||||||
Group: "end game",
|
Group: "end game",
|
||||||
URL: "https://my.go/app",
|
URL: "https://my.go/app",
|
||||||
},
|
},
|
||||||
Result: &core.Result{
|
Result: &endpoint.Result{
|
||||||
HTTPStatus: 400,
|
HTTPStatus: 400,
|
||||||
Hostname: "my.go",
|
Hostname: "my.go",
|
||||||
Errors: []string{"error 01", "error 02"},
|
Errors: []string{"error 01", "error 02"},
|
||||||
Success: false,
|
Success: false,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: false,
|
Success: false,
|
||||||
@ -279,14 +279,14 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
|||||||
Name string
|
Name string
|
||||||
Provider *AlertProvider
|
Provider *AlertProvider
|
||||||
Alert *alert.Alert
|
Alert *alert.Alert
|
||||||
Endpoint *core.Endpoint
|
Endpoint *endpoint.Endpoint
|
||||||
want alertCloseRequest
|
want alertCloseRequest
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "Missing all values",
|
Name: "Missing all values",
|
||||||
Provider: &AlertProvider{},
|
Provider: &AlertProvider{},
|
||||||
Alert: &alert.Alert{},
|
Alert: &alert.Alert{},
|
||||||
Endpoint: &core.Endpoint{},
|
Endpoint: &endpoint.Endpoint{},
|
||||||
want: alertCloseRequest{
|
want: alertCloseRequest{
|
||||||
Source: "",
|
Source: "",
|
||||||
Note: "RESOLVED: - ",
|
Note: "RESOLVED: - ",
|
||||||
@ -298,7 +298,7 @@ func TestAlertProvider_buildCloseRequestBody(t *testing.T) {
|
|||||||
Alert: &alert.Alert{
|
Alert: &alert.Alert{
|
||||||
Description: &description,
|
Description: &description,
|
||||||
},
|
},
|
||||||
Endpoint: &core.Endpoint{
|
Endpoint: &endpoint.Endpoint{
|
||||||
Name: "endpoint name",
|
Name: "endpoint name",
|
||||||
},
|
},
|
||||||
want: alertCloseRequest{
|
want: alertCloseRequest{
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
//
|
//
|
||||||
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -101,19 +101,19 @@ type Payload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, eventAction, resolveKey string
|
var message, eventAction, resolveKey string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
eventAction = "resolve"
|
eventAction = "resolve"
|
||||||
resolveKey = alert.ResolveKey
|
resolveKey = alert.ResolveKey
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
eventAction = "trigger"
|
eventAction = "trigger"
|
||||||
resolveKey = ""
|
resolveKey = ""
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
RoutingKey: provider.getIntegrationKeyForGroup(endpoint.Group),
|
RoutingKey: provider.getIntegrationKeyForGroup(ep.Group),
|
||||||
DedupKey: resolveKey,
|
DedupKey: resolveKey,
|
||||||
EventAction: eventAction,
|
EventAction: eventAction,
|
||||||
Payload: Payload{
|
Payload: Payload{
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -115,10 +115,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -161,7 +161,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(&core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &core.Result{}, scenario.Resolved)
|
body := scenario.Provider.buildRequestBody(&endpoint.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &endpoint.Result{}, scenario.Resolved)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/gitea"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||||
@ -19,9 +20,11 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
"github.com/TwiN/gatus/v5/alerting/provider/pushover"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
"github.com/TwiN/gatus/v5/alerting/provider/slack"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
"github.com/TwiN/gatus/v5/alerting/provider/teams"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/teamsworkflows"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/alerting/provider/zulip"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the interface that each provider should implement
|
// AlertProvider is the interface that each provider should implement
|
||||||
@ -33,7 +36,7 @@ type AlertProvider interface {
|
|||||||
GetDefaultAlert() *alert.Alert
|
GetDefaultAlert() *alert.Alert
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error
|
Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
||||||
@ -66,6 +69,7 @@ var (
|
|||||||
_ AlertProvider = (*email.AlertProvider)(nil)
|
_ AlertProvider = (*email.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*github.AlertProvider)(nil)
|
_ AlertProvider = (*github.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*gitlab.AlertProvider)(nil)
|
_ AlertProvider = (*gitlab.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*gitea.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*jetbrainsspace.AlertProvider)(nil)
|
_ AlertProvider = (*jetbrainsspace.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
||||||
@ -77,6 +81,8 @@ var (
|
|||||||
_ AlertProvider = (*pushover.AlertProvider)(nil)
|
_ AlertProvider = (*pushover.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*slack.AlertProvider)(nil)
|
_ AlertProvider = (*slack.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*teams.AlertProvider)(nil)
|
_ AlertProvider = (*teams.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*teamsworkflows.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*telegram.AlertProvider)(nil)
|
_ AlertProvider = (*telegram.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*twilio.AlertProvider)(nil)
|
_ AlertProvider = (*twilio.AlertProvider)(nil)
|
||||||
|
_ AlertProvider = (*zulip.AlertProvider)(nil)
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -52,8 +52,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
// Reference doc for pushover: https://pushover.net/api
|
// Reference doc for pushover: https://pushover.net/api
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -81,12 +81,12 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(Body{
|
body, _ := json.Marshal(Body{
|
||||||
Token: provider.ApplicationToken,
|
Token: provider.ApplicationToken,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -95,10 +95,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -150,10 +150,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Slack
|
// AlertProvider is the configuration necessary for sending an alert using Slack
|
||||||
@ -42,9 +42,9 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -81,13 +81,13 @@ type Field struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -142,7 +142,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
Provider AlertProvider
|
Provider AlertProvider
|
||||||
Endpoint core.Endpoint
|
Endpoint endpoint.Endpoint
|
||||||
Alert alert.Alert
|
Alert alert.Alert
|
||||||
NoConditions bool
|
NoConditions bool
|
||||||
Resolved bool
|
Resolved bool
|
||||||
@ -151,7 +151,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "triggered",
|
Name: "triggered",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@ -159,7 +159,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "triggered-with-group",
|
Name: "triggered-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@ -168,7 +168,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Name: "triggered-with-no-conditions",
|
Name: "triggered-with-no-conditions",
|
||||||
NoConditions: true,
|
NoConditions: true,
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}",
|
||||||
@ -176,7 +176,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "resolved",
|
Name: "resolved",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name"},
|
Endpoint: endpoint.Endpoint{Name: "name"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@ -184,7 +184,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "resolved-with-group",
|
Name: "resolved-with-group",
|
||||||
Provider: AlertProvider{},
|
Provider: AlertProvider{},
|
||||||
Endpoint: core.Endpoint{Name: "name", Group: "group"},
|
Endpoint: endpoint.Endpoint{Name: "name", Group: "group"},
|
||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}",
|
||||||
@ -192,9 +192,9 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
@ -202,7 +202,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&scenario.Endpoint,
|
&scenario.Endpoint,
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: conditionResults,
|
ConditionResults: conditionResults,
|
||||||
},
|
},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Teams
|
// AlertProvider is the configuration necessary for sending an alert using Teams
|
||||||
@ -19,8 +19,14 @@ 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"`
|
||||||
|
|
||||||
|
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||||
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
|
||||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||||
Overrides []Override `yaml:"overrides,omitempty"`
|
Overrides []Override `yaml:"overrides,omitempty"`
|
||||||
|
|
||||||
|
// Title is the title of the message that will be sent
|
||||||
|
Title string `yaml:"title,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override is a case under which the default integration is overridden
|
// Override is a case under which the default integration is overridden
|
||||||
@ -44,14 +50,14 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer)
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -78,13 +84,13 @@ type Section struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message, color string
|
var message, color string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
color = "#36A64F"
|
color = "#36A64F"
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
color = "#DD0000"
|
color = "#DD0000"
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
@ -105,9 +111,12 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
Type: "MessageCard",
|
Type: "MessageCard",
|
||||||
Context: "http://schema.org/extensions",
|
Context: "http://schema.org/extensions",
|
||||||
ThemeColor: color,
|
ThemeColor: color,
|
||||||
Title: "🚨 Gatus",
|
Title: provider.Title,
|
||||||
Text: message + description,
|
Text: message + description,
|
||||||
}
|
}
|
||||||
|
if len(body.Title) == 0 {
|
||||||
|
body.Title = "🚨 Gatus"
|
||||||
|
}
|
||||||
if len(formattedConditionResults) > 0 {
|
if len(formattedConditionResults) > 0 {
|
||||||
body.Sections = append(body.Sections, Section{
|
body.Sections = append(body.Sections, Section{
|
||||||
ActivityTitle: "Condition results",
|
ActivityTitle: "Condition results",
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -116,10 +116,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -172,17 +172,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{ConditionResults: conditionResults},
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
182
alerting/provider/teamsworkflows/teamsworkflows.go
Normal file
182
alerting/provider/teamsworkflows/teamsworkflows.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package teamsworkflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using Teams
|
||||||
|
type AlertProvider struct {
|
||||||
|
WebhookURL string `yaml:"webhook-url"`
|
||||||
|
|
||||||
|
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||||
|
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||||
|
|
||||||
|
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||||
|
Overrides []Override `yaml:"overrides,omitempty"`
|
||||||
|
|
||||||
|
// Title is the title of the message that will be sent
|
||||||
|
Title string `yaml:"title,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override is a case under which the default integration is overridden
|
||||||
|
type Override struct {
|
||||||
|
Group string `yaml:"group"`
|
||||||
|
WebhookURL string `yaml:"webhook-url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
registeredGroups := make(map[string]bool)
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
registeredGroups[override.Group] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(provider.WebhookURL) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an alert using the provider
|
||||||
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
|
request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
response, err := client.GetHTTPClient(nil).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode > 399 {
|
||||||
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdaptiveCardBody represents the structure of an Adaptive Card
|
||||||
|
type AdaptiveCardBody struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Body []CardBody `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardBody represents the body of the Adaptive Card
|
||||||
|
type CardBody struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Wrap bool `json:"wrap"`
|
||||||
|
Separator bool `json:"separator,omitempty"`
|
||||||
|
Size string `json:"size,omitempty"`
|
||||||
|
Weight string `json:"weight,omitempty"`
|
||||||
|
Items []CardBody `json:"items,omitempty"`
|
||||||
|
Facts []Fact `json:"facts,omitempty"`
|
||||||
|
FactSet *FactSetBody `json:"factSet,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FactSetBody represents the FactSet in the Adaptive Card
|
||||||
|
type FactSetBody struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Facts []Fact `json:"facts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fact represents an individual fact in the FactSet
|
||||||
|
type Fact struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRequestBody builds the request body for the provider
|
||||||
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
|
var message string
|
||||||
|
if resolved {
|
||||||
|
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row.", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row.", ep.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure default title if it's not provided
|
||||||
|
title := "⛑ Gatus"
|
||||||
|
if provider.Title != "" {
|
||||||
|
title = provider.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the facts from the condition results
|
||||||
|
var facts []Fact
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
var key string
|
||||||
|
if conditionResult.Success {
|
||||||
|
key = "✅"
|
||||||
|
} else {
|
||||||
|
key = "❌"
|
||||||
|
}
|
||||||
|
facts = append(facts, Fact{
|
||||||
|
Title: key,
|
||||||
|
Value: conditionResult.Condition,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cardContent := AdaptiveCardBody{
|
||||||
|
Type: "AdaptiveCard",
|
||||||
|
Version: "1.4", // Version 1.5 and 1.6 doesn't seem to be supported by Teams as of 27/08/2024
|
||||||
|
Body: []CardBody{
|
||||||
|
{
|
||||||
|
Type: "TextBlock",
|
||||||
|
Text: title,
|
||||||
|
Size: "Medium",
|
||||||
|
Weight: "Bolder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "TextBlock",
|
||||||
|
Text: message,
|
||||||
|
Wrap: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "FactSet",
|
||||||
|
Facts: facts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment := map[string]interface{}{
|
||||||
|
"contentType": "application/vnd.microsoft.card.adaptive",
|
||||||
|
"content": cardContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"type": "message",
|
||||||
|
"attachments": []interface{}{attachment},
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyAsJSON, _ := json.Marshal(payload)
|
||||||
|
return bodyAsJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
|
||||||
|
func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||||
|
if provider.Overrides != nil {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if group == override.Group {
|
||||||
|
return override.WebhookURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.WebhookURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
269
alerting/provider/teamsworkflows/teamsworkflows_test.go
Normal file
269
alerting/provider/teamsworkflows/teamsworkflows_test.go
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
package teamsworkflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{WebhookURL: ""}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
validProvider := AlertProvider{WebhookURL: "http://example.com"}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||||
|
providerWithInvalidOverrideGroup := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
WebhookURL: "http://example.com",
|
||||||
|
Group: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideGroup.IsValid() {
|
||||||
|
t.Error("provider Group shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithInvalidOverrideTo := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
WebhookURL: "",
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideTo.IsValid() {
|
||||||
|
t.Error("provider integration key shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithValidOverride := AlertProvider{
|
||||||
|
WebhookURL: "http://example.com",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
WebhookURL: "http://example.com",
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !providerWithValidOverride.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_Send(t *testing.T) {
|
||||||
|
defer client.InjectHTTPClient(nil)
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
MockRoundTripper test.MockRoundTripper
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "triggered-error",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-error",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
|
||||||
|
}),
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
|
err := scenario.Provider.Send(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if scenario.ExpectedError && err == nil {
|
||||||
|
t.Error("expected error, got none")
|
||||||
|
}
|
||||||
|
if !scenario.ExpectedError && err != nil {
|
||||||
|
t.Error("expected no error, got", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
NoConditions bool
|
||||||
|
Resolved bool
|
||||||
|
ExpectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedBody: "{\"attachments\":[{\"content\":{\"type\":\"AdaptiveCard\",\"version\":\"1.4\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"\\u0026#x26D1; Gatus\",\"wrap\":false,\"size\":\"Medium\",\"weight\":\"Bolder\"},{\"type\":\"TextBlock\",\"text\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row.\",\"wrap\":true},{\"type\":\"FactSet\",\"wrap\":false,\"facts\":[{\"title\":\"\\u0026#x274C;\",\"value\":\"[CONNECTED] == true\"},{\"title\":\"\\u0026#x274C;\",\"value\":\"[STATUS] == 200\"}]}]},\"contentType\":\"application/vnd.microsoft.card.adaptive\"}],\"type\":\"message\"}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: "{\"attachments\":[{\"content\":{\"type\":\"AdaptiveCard\",\"version\":\"1.4\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"\\u0026#x26D1; Gatus\",\"wrap\":false,\"size\":\"Medium\",\"weight\":\"Bolder\"},{\"type\":\"TextBlock\",\"text\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row.\",\"wrap\":true},{\"type\":\"FactSet\",\"wrap\":false,\"facts\":[{\"title\":\"\\u0026#x2705;\",\"value\":\"[CONNECTED] == true\"},{\"title\":\"\\u0026#x2705;\",\"value\":\"[STATUS] == 200\"}]}]},\"contentType\":\"application/vnd.microsoft.card.adaptive\"}],\"type\":\"message\"}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved-with-no-conditions",
|
||||||
|
NoConditions: true,
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedBody: "{\"attachments\":[{\"content\":{\"type\":\"AdaptiveCard\",\"version\":\"1.4\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"\\u0026#x26D1; Gatus\",\"wrap\":false,\"size\":\"Medium\",\"weight\":\"Bolder\"},{\"type\":\"TextBlock\",\"text\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row.\",\"wrap\":true},{\"type\":\"FactSet\",\"wrap\":false}]},\"contentType\":\"application/vnd.microsoft.card.adaptive\"}],\"type\":\"message\"}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if !scenario.NoConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body := scenario.Provider.buildRequestBody(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||||
|
}
|
||||||
|
out := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(body, &out); err != nil {
|
||||||
|
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
|
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
|
t.Error("expected default alert to be not nil")
|
||||||
|
}
|
||||||
|
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
|
t.Error("expected default alert to be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultAPIURL = "https://api.telegram.org"
|
const defaultAPIURL = "https://api.telegram.org"
|
||||||
@ -25,6 +25,16 @@ 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 Overrid that may be prioritized over the default configuration
|
||||||
|
Overrides []*Override `yaml:"overrides,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override is a configuration that may be prioritized over the default configuration
|
||||||
|
type Override struct {
|
||||||
|
group string `yaml:"group"`
|
||||||
|
token string `yaml:"token"`
|
||||||
|
id string `yaml:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns whether the provider's configuration is valid
|
// IsValid returns whether the provider's configuration is valid
|
||||||
@ -32,17 +42,29 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
if provider.ClientConfig == nil {
|
if provider.ClientConfig == nil {
|
||||||
provider.ClientConfig = client.GetDefaultConfig()
|
provider.ClientConfig = client.GetDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerGroups := make(map[string]bool)
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if len(override.group) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := registerGroups[override.group]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
registerGroups[override.group] = true
|
||||||
|
}
|
||||||
|
|
||||||
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(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
apiURL := provider.APIURL
|
apiURL := provider.APIURL
|
||||||
if apiURL == "" {
|
if apiURL == "" {
|
||||||
apiURL = defaultAPIURL
|
apiURL = defaultAPIURL
|
||||||
}
|
}
|
||||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/bot%s/sendMessage", apiURL, provider.Token), buffer)
|
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/bot%s/sendMessage", apiURL, provider.getTokenForGroup(ep.Group)), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -59,6 +81,15 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *AlertProvider) getTokenForGroup(group string) string {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if override.group == group && len(override.token) > 0 {
|
||||||
|
return override.token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.Token
|
||||||
|
}
|
||||||
|
|
||||||
type Body struct {
|
type Body struct {
|
||||||
ChatID string `json:"chat_id"`
|
ChatID string `json:"chat_id"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
@ -66,12 +97,12 @@ type Body struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
if len(result.ConditionResults) > 0 {
|
if len(result.ConditionResults) > 0 {
|
||||||
@ -93,13 +124,22 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
|||||||
text = fmt.Sprintf("⛑ *Gatus* \n%s%s", message, formattedConditionResults)
|
text = fmt.Sprintf("⛑ *Gatus* \n%s%s", message, formattedConditionResults)
|
||||||
}
|
}
|
||||||
bodyAsJSON, _ := json.Marshal(Body{
|
bodyAsJSON, _ := json.Marshal(Body{
|
||||||
ChatID: provider.ID,
|
ChatID: provider.getIDForGroup(ep.Group),
|
||||||
Text: text,
|
Text: text,
|
||||||
ParseMode: "MARKDOWN",
|
ParseMode: "MARKDOWN",
|
||||||
})
|
})
|
||||||
return bodyAsJSON
|
return bodyAsJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (provider *AlertProvider) getIDForGroup(group string) string {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if override.group == group && len(override.id) > 0 {
|
||||||
|
return override.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.ID
|
||||||
|
}
|
||||||
|
|
||||||
// GetDefaultAlert returns the provider's default alert configuration
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
return provider.DefaultAlert
|
return provider.DefaultAlert
|
||||||
|
@ -7,11 +7,11 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertProvider_IsValid(t *testing.T) {
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
t.Run("invalid-provider", func(t *testing.T) {
|
t.Run("invalid-provider", func(t *testing.T) {
|
||||||
invalidProvider := AlertProvider{Token: "", ID: ""}
|
invalidProvider := AlertProvider{Token: "", ID: ""}
|
||||||
if invalidProvider.IsValid() {
|
if invalidProvider.IsValid() {
|
||||||
@ -32,6 +32,69 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValidWithOverrides(t *testing.T) {
|
||||||
|
t.Run("invalid-provider-override-nonexist-group", func(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678", Overrides: []*Override{{token: "token", id: "id"}}}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("invalid-provider-override-duplicate-group", func(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678", Overrides: []*Override{{group: "group1", token: "token", id: "id"}, {group: "group1", id: "id2"}}}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("valid-provider", func(t *testing.T) {
|
||||||
|
validProvider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678", Overrides: []*Override{{group: "group", token: "token", id: "id"}}}
|
||||||
|
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_getTokenAndIDForGroup(t *testing.T) {
|
||||||
|
t.Run("get-token-with-override", func(t *testing.T) {
|
||||||
|
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678", Overrides: []*Override{{group: "group", token: "overrideToken", id: "overrideID"}}}
|
||||||
|
token := provider.getTokenForGroup("group")
|
||||||
|
if token != "overrideToken" {
|
||||||
|
t.Error("token should have been 'overrideToken'")
|
||||||
|
}
|
||||||
|
id := provider.getIDForGroup("group")
|
||||||
|
if id != "overrideID" {
|
||||||
|
t.Error("id should have been 'overrideID'")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("get-default-token-with-overridden-id", func(t *testing.T) {
|
||||||
|
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678", Overrides: []*Override{{group: "group", id: "overrideID"}}}
|
||||||
|
token := provider.getTokenForGroup("group")
|
||||||
|
if token != provider.Token {
|
||||||
|
t.Error("token should have been the default token")
|
||||||
|
}
|
||||||
|
id := provider.getIDForGroup("group")
|
||||||
|
if id != "overrideID" {
|
||||||
|
t.Error("id should have been 'overrideID'")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("get-default-token-with-overridden-token", func(t *testing.T) {
|
||||||
|
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678", Overrides: []*Override{{group: "group", token: "overrideToken"}}}
|
||||||
|
token := provider.getTokenForGroup("group")
|
||||||
|
if token != "overrideToken" {
|
||||||
|
t.Error("token should have been 'overrideToken'")
|
||||||
|
}
|
||||||
|
id := provider.getIDForGroup("group")
|
||||||
|
if id != provider.ID {
|
||||||
|
t.Error("id should have been the default id")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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"
|
||||||
@ -89,10 +152,10 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||||
err := scenario.Provider.Send(
|
err := scenario.Provider.Send(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
@ -145,17 +208,17 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
var conditionResults []*core.ConditionResult
|
var conditionResults []*endpoint.ConditionResult
|
||||||
if !scenario.NoConditions {
|
if !scenario.NoConditions {
|
||||||
conditionResults = []*core.ConditionResult{
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{ConditionResults: conditionResults},
|
&endpoint.Result{ConditionResults: conditionResults},
|
||||||
scenario.Resolved,
|
scenario.Resolved,
|
||||||
)
|
)
|
||||||
if string(body) != scenario.ExpectedBody {
|
if string(body) != scenario.ExpectedBody {
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
// AlertProvider is the configuration necessary for sending an alert using Twilio
|
||||||
@ -30,8 +30,8 @@ func (provider *AlertProvider) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send an alert using the provider
|
// Send an alert using the provider
|
||||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
|
||||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -51,12 +51,12 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestBody builds the request body for the provider
|
// buildRequestBody builds the request body for the provider
|
||||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
var message string
|
var message string
|
||||||
if resolved {
|
if resolved {
|
||||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
} else {
|
} else {
|
||||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
|
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
|
||||||
}
|
}
|
||||||
return url.Values{
|
return url.Values{
|
||||||
"To": {provider.To},
|
"To": {provider.To},
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
||||||
@ -51,10 +51,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
body := scenario.Provider.buildRequestBody(
|
body := scenario.Provider.buildRequestBody(
|
||||||
&core.Endpoint{Name: "endpoint-name"},
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
&scenario.Alert,
|
&scenario.Alert,
|
||||||
&core.Result{
|
&endpoint.Result{
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
},
|
},
|
||||||
|
132
alerting/provider/zulip/zulip.go
Normal file
132
alerting/provider/zulip/zulip.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package zulip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// BotEmail is the email of the bot user
|
||||||
|
BotEmail string `yaml:"bot-email"`
|
||||||
|
// BotAPIKey is the API key of the bot user
|
||||||
|
BotAPIKey string `yaml:"bot-api-key"`
|
||||||
|
// Domain is the domain of the Zulip server
|
||||||
|
Domain string `yaml:"domain"`
|
||||||
|
// ChannelID is the ID of the channel to send the message to
|
||||||
|
ChannelID string `yaml:"channel-id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using Zulip
|
||||||
|
type AlertProvider struct {
|
||||||
|
Config `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 {
|
||||||
|
Config
|
||||||
|
Group string `yaml:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *AlertProvider) validateConfig(conf *Config) bool {
|
||||||
|
return len(conf.BotEmail) > 0 && len(conf.BotAPIKey) > 0 && len(conf.Domain) > 0 && len(conf.ChannelID) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
isAlreadyRegistered := registeredGroups[override.Group]
|
||||||
|
if isAlreadyRegistered || override.Group == "" || !provider.validateConfig(&override.Config) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
registeredGroups[override.Group] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.validateConfig(&provider.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getChannelIdForGroup returns the channel ID for the provided group
|
||||||
|
func (provider *AlertProvider) getChannelIdForGroup(group string) string {
|
||||||
|
for _, override := range provider.Overrides {
|
||||||
|
if override.Group == group {
|
||||||
|
return override.ChannelID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider.ChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRequestBody builds the request body for the provider
|
||||||
|
func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
|
||||||
|
var message string
|
||||||
|
if resolved {
|
||||||
|
message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
|
message += "\n> " + alertDescription + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
var prefix string
|
||||||
|
if conditionResult.Success {
|
||||||
|
prefix = ":check:"
|
||||||
|
} else {
|
||||||
|
prefix = ":cross_mark:"
|
||||||
|
}
|
||||||
|
message += fmt.Sprintf("\n%s - `%s`", prefix, conditionResult.Condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
postData := map[string]string{
|
||||||
|
"type": "channel",
|
||||||
|
"to": provider.getChannelIdForGroup(ep.Group),
|
||||||
|
"topic": "Gatus",
|
||||||
|
"content": message,
|
||||||
|
}
|
||||||
|
bodyParams := url.Values{}
|
||||||
|
for field, value := range postData {
|
||||||
|
bodyParams.Add(field, value)
|
||||||
|
}
|
||||||
|
return bodyParams.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an alert using the provider
|
||||||
|
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
|
||||||
|
buffer := bytes.NewBufferString(provider.buildRequestBody(ep, alert, result, resolved))
|
||||||
|
zulipEndpoint := fmt.Sprintf("https://%s/api/v1/messages", provider.Domain)
|
||||||
|
request, err := http.NewRequest(http.MethodPost, zulipEndpoint, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.SetBasicAuth(provider.BotEmail, provider.BotAPIKey)
|
||||||
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
request.Header.Set("User-Agent", "Gatus")
|
||||||
|
response, err := client.GetHTTPClient(nil).Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode > 399 {
|
||||||
|
body, _ := io.ReadAll(response.Body)
|
||||||
|
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAlert returns the provider's default alert configuration
|
||||||
|
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||||
|
return provider.DefaultAlert
|
||||||
|
}
|
488
alerting/provider/zulip/zulip_test.go
Normal file
488
alerting/provider/zulip/zulip_test.go
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
package zulip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
|
"github.com/TwiN/gatus/v5/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValid(t *testing.T) {
|
||||||
|
testCase := []struct {
|
||||||
|
name string
|
||||||
|
alertProvider AlertProvider
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty provider",
|
||||||
|
alertProvider: AlertProvider{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty channel id",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
BotAPIKey: "something",
|
||||||
|
Domain: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty domain",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
BotAPIKey: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty bot api key",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
Domain: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty bot email",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: Config{
|
||||||
|
BotAPIKey: "something",
|
||||||
|
Domain: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid provider",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
BotAPIKey: "something",
|
||||||
|
Domain: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCase {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.alertProvider.IsValid() != tc.expected {
|
||||||
|
t.Errorf("IsValid assertion failed (expected %v, got %v)", tc.expected, !tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||||
|
validConfig := Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
BotAPIKey: "something",
|
||||||
|
Domain: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase := []struct {
|
||||||
|
name string
|
||||||
|
alertProvider AlertProvider
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty group",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Config: validConfig,
|
||||||
|
Group: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty override config",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty channel id",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "something",
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
BotAPIKey: "something",
|
||||||
|
Domain: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty domain",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "something",
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
BotAPIKey: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty bot api key",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "something",
|
||||||
|
Config: Config{
|
||||||
|
BotEmail: "something",
|
||||||
|
Domain: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty bot email",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "something",
|
||||||
|
Config: Config{
|
||||||
|
BotAPIKey: "something",
|
||||||
|
Domain: "something",
|
||||||
|
ChannelID: "something",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid provider",
|
||||||
|
alertProvider: AlertProvider{
|
||||||
|
Config: validConfig,
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "something",
|
||||||
|
Config: validConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCase {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.alertProvider.IsValid() != tc.expected {
|
||||||
|
t.Errorf("IsValid assertion failed (expected %v, got %v)", tc.expected, !tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_GetChannelIdForGroup(t *testing.T) {
|
||||||
|
provider := AlertProvider{
|
||||||
|
Config: Config{
|
||||||
|
ChannelID: "default",
|
||||||
|
},
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group1",
|
||||||
|
Config: Config{ChannelID: "group1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Group: "group2",
|
||||||
|
Config: Config{ChannelID: "group2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if provider.getChannelIdForGroup("") != "default" {
|
||||||
|
t.Error("Expected default channel ID")
|
||||||
|
}
|
||||||
|
if provider.getChannelIdForGroup("group2") != "group2" {
|
||||||
|
t.Error("Expected group2 channel ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_BuildRequestBody(t *testing.T) {
|
||||||
|
basicConfig := Config{
|
||||||
|
BotEmail: "bot-email",
|
||||||
|
BotAPIKey: "bot-api-key",
|
||||||
|
Domain: "domain",
|
||||||
|
ChannelID: "channel-id",
|
||||||
|
}
|
||||||
|
alertDesc := "Description"
|
||||||
|
basicAlert := alert.Alert{
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
FailureThreshold: 3,
|
||||||
|
Description: &alertDesc,
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
provider AlertProvider
|
||||||
|
alert alert.Alert
|
||||||
|
resolved bool
|
||||||
|
hasConditions bool
|
||||||
|
expectedBody url.Values
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Resolved alert with no conditions",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: true,
|
||||||
|
hasConditions: false,
|
||||||
|
expectedBody: url.Values{
|
||||||
|
"content": {`An alert for **endpoint-name** has been resolved after passing successfully 2 time(s) in a row
|
||||||
|
> Description
|
||||||
|
`},
|
||||||
|
"to": {"channel-id"},
|
||||||
|
"topic": {"Gatus"},
|
||||||
|
"type": {"channel"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Resolved alert with conditions",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: true,
|
||||||
|
hasConditions: true,
|
||||||
|
expectedBody: url.Values{
|
||||||
|
"content": {`An alert for **endpoint-name** has been resolved after passing successfully 2 time(s) in a row
|
||||||
|
> Description
|
||||||
|
|
||||||
|
:check: - ` + "`[CONNECTED] == true`" + `
|
||||||
|
:check: - ` + "`[STATUS] == 200`" + `
|
||||||
|
:check: - ` + "`[BODY] != \"\"`"},
|
||||||
|
"to": {"channel-id"},
|
||||||
|
"topic": {"Gatus"},
|
||||||
|
"type": {"channel"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failed alert with no conditions",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: false,
|
||||||
|
hasConditions: false,
|
||||||
|
expectedBody: url.Values{
|
||||||
|
"content": {`An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row
|
||||||
|
> Description
|
||||||
|
`},
|
||||||
|
"to": {"channel-id"},
|
||||||
|
"topic": {"Gatus"},
|
||||||
|
"type": {"channel"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failed alert with conditions",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: false,
|
||||||
|
hasConditions: true,
|
||||||
|
expectedBody: url.Values{
|
||||||
|
"content": {`An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row
|
||||||
|
> Description
|
||||||
|
|
||||||
|
:cross_mark: - ` + "`[CONNECTED] == true`" + `
|
||||||
|
:cross_mark: - ` + "`[STATUS] == 200`" + `
|
||||||
|
:cross_mark: - ` + "`[BODY] != \"\"`"},
|
||||||
|
"to": {"channel-id"},
|
||||||
|
"topic": {"Gatus"},
|
||||||
|
"type": {"channel"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var conditionResults []*endpoint.ConditionResult
|
||||||
|
if tc.hasConditions {
|
||||||
|
conditionResults = []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: tc.resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: tc.resolved},
|
||||||
|
{Condition: "[BODY] != \"\"", Success: tc.resolved},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body := tc.provider.buildRequestBody(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&tc.alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: conditionResults,
|
||||||
|
},
|
||||||
|
tc.resolved,
|
||||||
|
)
|
||||||
|
valuesResult, err := url.ParseQuery(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if fmt.Sprintf("%v", valuesResult) != fmt.Sprintf("%v", tc.expectedBody) {
|
||||||
|
t.Errorf("Expected body:\n%v\ngot:\n%v", tc.expectedBody, valuesResult)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_Send(t *testing.T) {
|
||||||
|
defer client.InjectHTTPClient(nil)
|
||||||
|
validateRequest := func(req *http.Request) {
|
||||||
|
if req.URL.String() != "https://custom-domain/api/v1/messages" {
|
||||||
|
t.Errorf("expected url https://custom-domain.zulipchat.com/api/v1/messages, got %s", req.URL.String())
|
||||||
|
}
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
t.Errorf("expected POST request, got %s", req.Method)
|
||||||
|
}
|
||||||
|
if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
|
||||||
|
t.Errorf("expected Content-Type header to be application/x-www-form-urlencoded, got %s", req.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
if req.Header.Get("User-Agent") != "Gatus" {
|
||||||
|
t.Errorf("expected User-Agent header to be Gatus, got %s", req.Header.Get("User-Agent"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
basicConfig := Config{
|
||||||
|
BotEmail: "bot-email",
|
||||||
|
BotAPIKey: "bot-api-key",
|
||||||
|
Domain: "custom-domain",
|
||||||
|
ChannelID: "channel-id",
|
||||||
|
}
|
||||||
|
basicAlert := alert.Alert{
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
FailureThreshold: 3,
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
provider AlertProvider
|
||||||
|
alert alert.Alert
|
||||||
|
resolved bool
|
||||||
|
mockRoundTripper test.MockRoundTripper
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "resolved",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: true,
|
||||||
|
mockRoundTripper: test.MockRoundTripper(func(req *http.Request) *http.Response {
|
||||||
|
validateRequest(req)
|
||||||
|
return &http.Response{StatusCode: http.StatusOK}
|
||||||
|
}),
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolved error",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: true,
|
||||||
|
mockRoundTripper: test.MockRoundTripper(func(req *http.Request) *http.Response {
|
||||||
|
validateRequest(req)
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError}
|
||||||
|
}),
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "triggered",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: false,
|
||||||
|
mockRoundTripper: test.MockRoundTripper(func(req *http.Request) *http.Response {
|
||||||
|
validateRequest(req)
|
||||||
|
return &http.Response{StatusCode: http.StatusOK}
|
||||||
|
}),
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "triggered error",
|
||||||
|
provider: AlertProvider{
|
||||||
|
Config: basicConfig,
|
||||||
|
},
|
||||||
|
alert: basicAlert,
|
||||||
|
resolved: false,
|
||||||
|
mockRoundTripper: test.MockRoundTripper(func(req *http.Request) *http.Response {
|
||||||
|
validateRequest(req)
|
||||||
|
return &http.Response{StatusCode: http.StatusInternalServerError}
|
||||||
|
}),
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
client.InjectHTTPClient(&http.Client{Transport: tc.mockRoundTripper})
|
||||||
|
err := tc.provider.Send(
|
||||||
|
&endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
|
&tc.alert,
|
||||||
|
&endpoint.Result{
|
||||||
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: tc.resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: tc.resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tc.resolved,
|
||||||
|
)
|
||||||
|
if tc.expectedError && err == nil {
|
||||||
|
t.Error("expected error, got none")
|
||||||
|
}
|
||||||
|
if !tc.expectedError && err != nil {
|
||||||
|
t.Errorf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
18
api/badge.go
18
api/badge.go
@ -9,7 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
@ -37,11 +37,13 @@ var (
|
|||||||
|
|
||||||
// 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 -> 30d, 7d, 24h, 1h
|
||||||
func UptimeBadge(c *fiber.Ctx) error {
|
func UptimeBadge(c *fiber.Ctx) error {
|
||||||
duration := c.Params("duration")
|
duration := c.Params("duration")
|
||||||
var from time.Time
|
var from time.Time
|
||||||
switch duration {
|
switch duration {
|
||||||
|
case "30d":
|
||||||
|
from = time.Now().Add(-30 * 24 * time.Hour)
|
||||||
case "7d":
|
case "7d":
|
||||||
from = time.Now().Add(-7 * 24 * time.Hour)
|
from = time.Now().Add(-7 * 24 * time.Hour)
|
||||||
case "24h":
|
case "24h":
|
||||||
@ -49,7 +51,7 @@ func UptimeBadge(c *fiber.Ctx) error {
|
|||||||
case "1h":
|
case "1h":
|
||||||
from = time.Now().Add(-2 * time.Hour) // Because uptime metrics are stored by hour, we have to cheat a little
|
from = time.Now().Add(-2 * time.Hour) // Because uptime metrics are stored by hour, we have to cheat a little
|
||||||
default:
|
default:
|
||||||
return c.Status(400).SendString("Durations supported: 7d, 24h, 1h")
|
return c.Status(400).SendString("Durations supported: 30d, 7d, 24h, 1h")
|
||||||
}
|
}
|
||||||
key := c.Params("key")
|
key := c.Params("key")
|
||||||
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
|
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
|
||||||
@ -69,12 +71,14 @@ func UptimeBadge(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||||
//
|
//
|
||||||
// Valid values for :duration -> 7d, 24h, 1h
|
// Valid values for :duration -> 30d, 7d, 24h, 1h
|
||||||
func ResponseTimeBadge(cfg *config.Config) fiber.Handler {
|
func ResponseTimeBadge(cfg *config.Config) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
duration := c.Params("duration")
|
duration := c.Params("duration")
|
||||||
var from time.Time
|
var from time.Time
|
||||||
switch duration {
|
switch duration {
|
||||||
|
case "30d":
|
||||||
|
from = time.Now().Add(-30 * 24 * time.Hour)
|
||||||
case "7d":
|
case "7d":
|
||||||
from = time.Now().Add(-7 * 24 * time.Hour)
|
from = time.Now().Add(-7 * 24 * time.Hour)
|
||||||
case "24h":
|
case "24h":
|
||||||
@ -82,7 +86,7 @@ func ResponseTimeBadge(cfg *config.Config) fiber.Handler {
|
|||||||
case "1h":
|
case "1h":
|
||||||
from = time.Now().Add(-2 * time.Hour) // Because response time metrics are stored by hour, we have to cheat a little
|
from = time.Now().Add(-2 * time.Hour) // Because response time metrics are stored by hour, we have to cheat a little
|
||||||
default:
|
default:
|
||||||
return c.Status(400).SendString("Durations supported: 7d, 24h, 1h")
|
return c.Status(400).SendString("Durations supported: 30d, 7d, 24h, 1h")
|
||||||
}
|
}
|
||||||
key := c.Params("key")
|
key := c.Params("key")
|
||||||
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||||
@ -161,6 +165,8 @@ func HealthBadgeShields(c *fiber.Ctx) error {
|
|||||||
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
||||||
var labelWidth, valueWidth, valueWidthAdjustment int
|
var labelWidth, valueWidth, valueWidthAdjustment int
|
||||||
switch duration {
|
switch duration {
|
||||||
|
case "30d":
|
||||||
|
labelWidth = 70
|
||||||
case "7d":
|
case "7d":
|
||||||
labelWidth = 65
|
labelWidth = 65
|
||||||
case "24h":
|
case "24h":
|
||||||
@ -227,6 +233,8 @@ func getBadgeColorFromUptime(uptime float64) string {
|
|||||||
func generateResponseTimeBadgeSVG(duration string, averageResponseTime int, key string, cfg *config.Config) []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 "30d":
|
||||||
|
labelWidth = 110
|
||||||
case "7d":
|
case "7d":
|
||||||
labelWidth = 105
|
labelWidth = 105
|
||||||
case "24h":
|
case "24h":
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@ -19,7 +19,7 @@ func TestBadge(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@ -34,8 +34,8 @@ func TestBadge(t *testing.T) {
|
|||||||
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
||||||
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
||||||
|
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
@ -218,30 +218,30 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
firstCondition = core.Condition("[STATUS] == 200")
|
firstCondition = endpoint.Condition("[STATUS] == 200")
|
||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
secondCondition = endpoint.Condition("[RESPONSE_TIME] < 500")
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
thirdCondition = endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
)
|
)
|
||||||
|
|
||||||
firstTestEndpoint := core.Endpoint{
|
firstTestEndpoint := endpoint.Endpoint{
|
||||||
Name: "a",
|
Name: "a",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
UIConfig: ui.GetDefaultConfig(),
|
UIConfig: ui.GetDefaultConfig(),
|
||||||
}
|
}
|
||||||
secondTestEndpoint := core.Endpoint{
|
secondTestEndpoint := endpoint.Endpoint{
|
||||||
Name: "b",
|
Name: "b",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
Conditions: []endpoint.Condition{firstCondition, secondCondition, thirdCondition},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
@ -255,10 +255,10 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
Endpoints: []*endpoint.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||||
}
|
}
|
||||||
|
|
||||||
testSuccessfulResult := core.Result{
|
testSuccessfulResult := endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
@ -268,7 +268,7 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
|||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
10
api/chart.go
10
api/chart.go
@ -32,14 +32,18 @@ var (
|
|||||||
|
|
||||||
func ResponseTimeChart(c *fiber.Ctx) error {
|
func ResponseTimeChart(c *fiber.Ctx) error {
|
||||||
duration := c.Params("duration")
|
duration := c.Params("duration")
|
||||||
|
chartTimestampFormatter := chart.TimeValueFormatterWithFormat(timeFormat)
|
||||||
var from time.Time
|
var from time.Time
|
||||||
switch duration {
|
switch duration {
|
||||||
|
case "30d":
|
||||||
|
from = time.Now().Truncate(time.Hour).Add(-30 * 24 * time.Hour)
|
||||||
|
chartTimestampFormatter = chart.TimeDateValueFormatter
|
||||||
case "7d":
|
case "7d":
|
||||||
from = time.Now().Truncate(time.Hour).Add(-24 * 7 * time.Hour)
|
from = time.Now().Truncate(time.Hour).Add(-7 * 24 * time.Hour)
|
||||||
case "24h":
|
case "24h":
|
||||||
from = time.Now().Truncate(time.Hour).Add(-24 * time.Hour)
|
from = time.Now().Truncate(time.Hour).Add(-24 * time.Hour)
|
||||||
default:
|
default:
|
||||||
return c.Status(400).SendString("Durations supported: 7d, 24h")
|
return c.Status(400).SendString("Durations supported: 30d, 7d, 24h")
|
||||||
}
|
}
|
||||||
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(c.Params("key"), from, time.Now())
|
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(c.Params("key"), from, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -88,7 +92,7 @@ func ResponseTimeChart(c *fiber.Ctx) error {
|
|||||||
Width: 1280,
|
Width: 1280,
|
||||||
Height: 300,
|
Height: 300,
|
||||||
XAxis: chart.XAxis{
|
XAxis: chart.XAxis{
|
||||||
ValueFormatter: chart.TimeValueFormatterWithFormat(timeFormat),
|
ValueFormatter: chartTimestampFormatter,
|
||||||
GridMajorStyle: gridStyle,
|
GridMajorStyle: gridStyle,
|
||||||
GridMinorStyle: gridStyle,
|
GridMinorStyle: gridStyle,
|
||||||
Style: axisStyle,
|
Style: axisStyle,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@ -17,7 +17,7 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@ -28,8 +28,8 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
@ -49,6 +49,11 @@ func TestResponseTimeChart(t *testing.T) {
|
|||||||
Path: "/api/v1/endpoints/core_frontend/response-times/7d/chart.svg",
|
Path: "/api/v1/endpoints/core_frontend/response-times/7d/chart.svg",
|
||||||
ExpectedCode: http.StatusOK,
|
ExpectedCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "chart-response-time-30d",
|
||||||
|
Path: "/api/v1/endpoints/core_frontend/response-times/30d/chart.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "chart-response-time-with-invalid-duration",
|
Name: "chart-response-time-with-invalid-duration",
|
||||||
Path: "/api/v1/endpoints/core_backend/response-times/3d/chart.svg",
|
Path: "/api/v1/endpoints/core_backend/response-times/3d/chart.svg",
|
||||||
|
@ -4,13 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/remote"
|
"github.com/TwiN/gatus/v5/config/remote"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
@ -51,25 +50,19 @@ func EndpointStatuses(cfg *config.Config) fiber.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*core.EndpointStatus, error) {
|
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*endpoint.Status, error) {
|
||||||
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var endpointStatusesFromAllRemotes []*core.EndpointStatus
|
var endpointStatusesFromAllRemotes []*endpoint.Status
|
||||||
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
|
||||||
for _, instance := range remoteConfig.Instances {
|
for _, instance := range remoteConfig.Instances {
|
||||||
response, err := httpClient.Get(instance.URL)
|
response, err := httpClient.Get(instance.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(response.Body)
|
var endpointStatuses []*endpoint.Status
|
||||||
if err != nil {
|
if err = json.NewDecoder(response.Body).Decode(&endpointStatuses); err != nil {
|
||||||
_ = response.Body.Close()
|
|
||||||
log.Printf("[api.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()
|
_ = response.Body.Close()
|
||||||
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
log.Printf("[api.getEndpointStatusesFromRemoteInstances] Silently failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
|
||||||
continue
|
continue
|
||||||
@ -83,7 +76,7 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
|
|||||||
return endpointStatusesFromAllRemotes, nil
|
return endpointStatusesFromAllRemotes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name
|
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name
|
||||||
func EndpointStatus(c *fiber.Ctx) error {
|
func EndpointStatus(c *fiber.Ctx) error {
|
||||||
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
||||||
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@ -16,19 +16,19 @@ import (
|
|||||||
var (
|
var (
|
||||||
timestamp = time.Now()
|
timestamp = time.Now()
|
||||||
|
|
||||||
testEndpoint = core.Endpoint{
|
testEndpoint = endpoint.Endpoint{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Group: "group",
|
Group: "group",
|
||||||
URL: "https://example.org/what/ever",
|
URL: "https://example.org/what/ever",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Body: "body",
|
Body: "body",
|
||||||
Interval: 30 * time.Second,
|
Interval: 30 * time.Second,
|
||||||
Conditions: []core.Condition{core.Condition("[STATUS] == 200"), core.Condition("[RESPONSE_TIME] < 500"), core.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
Conditions: []endpoint.Condition{endpoint.Condition("[STATUS] == 200"), endpoint.Condition("[RESPONSE_TIME] < 500"), endpoint.Condition("[CERTIFICATE_EXPIRATION] < 72h")},
|
||||||
Alerts: nil,
|
Alerts: nil,
|
||||||
NumberOfFailuresInARow: 0,
|
NumberOfFailuresInARow: 0,
|
||||||
NumberOfSuccessesInARow: 0,
|
NumberOfSuccessesInARow: 0,
|
||||||
}
|
}
|
||||||
testSuccessfulResult = core.Result{
|
testSuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
@ -38,7 +38,7 @@ var (
|
|||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@ -53,7 +53,7 @@ var (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testUnsuccessfulResult = core.Result{
|
testUnsuccessfulResult = endpoint.Result{
|
||||||
Hostname: "example.org",
|
Hostname: "example.org",
|
||||||
IP: "127.0.0.1",
|
IP: "127.0.0.1",
|
||||||
HTTPStatus: 200,
|
HTTPStatus: 200,
|
||||||
@ -63,7 +63,7 @@ var (
|
|||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Duration: 750 * time.Millisecond,
|
Duration: 750 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*endpoint.ConditionResult{
|
||||||
{
|
{
|
||||||
Condition: "[STATUS] == 200",
|
Condition: "[STATUS] == 200",
|
||||||
Success: true,
|
Success: true,
|
||||||
@ -85,7 +85,7 @@ func TestEndpointStatus(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@ -96,8 +96,8 @@ func TestEndpointStatus(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
@ -41,11 +41,14 @@ func CreateExternalEndpointResult(cfg *config.Config) fiber.Handler {
|
|||||||
return c.Status(401).SendString("invalid token")
|
return c.Status(401).SendString("invalid token")
|
||||||
}
|
}
|
||||||
// Persist the result in the storage
|
// Persist the result in the storage
|
||||||
result := &core.Result{
|
result := &endpoint.Result{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Success: c.QueryBool("success"),
|
Success: c.QueryBool("success"),
|
||||||
Errors: []string{},
|
Errors: []string{},
|
||||||
}
|
}
|
||||||
|
if !result.Success && c.Query("error") != "" {
|
||||||
|
result.Errors = append(result.Errors, c.Query("error"))
|
||||||
|
}
|
||||||
convertedEndpoint := externalEndpoint.ToEndpoint()
|
convertedEndpoint := externalEndpoint.ToEndpoint()
|
||||||
if err := store.Get().Insert(convertedEndpoint, result); err != nil {
|
if err := store.Get().Insert(convertedEndpoint, result); err != nil {
|
||||||
if errors.Is(err, common.ErrEndpointNotFound) {
|
if errors.Is(err, common.ErrEndpointNotFound) {
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
@ -22,7 +22,7 @@ func TestCreateExternalEndpointResult(t *testing.T) {
|
|||||||
Alerting: &alerting.Config{
|
Alerting: &alerting.Config{
|
||||||
Discord: &discord.AlertProvider{},
|
Discord: &discord.AlertProvider{},
|
||||||
},
|
},
|
||||||
ExternalEndpoints: []*core.ExternalEndpoint{
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
||||||
{
|
{
|
||||||
Name: "n",
|
Name: "n",
|
||||||
Group: "g",
|
Group: "g",
|
||||||
@ -64,12 +64,24 @@ func TestCreateExternalEndpointResult(t *testing.T) {
|
|||||||
AuthorizationHeaderBearerToken: "Bearer token",
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
ExpectedCode: 404,
|
ExpectedCode: 404,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "bad-success-value",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=invalid",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 400,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "good-token-success-true",
|
Name: "good-token-success-true",
|
||||||
Path: "/api/v1/endpoints/g_n/external?success=true",
|
Path: "/api/v1/endpoints/g_n/external?success=true",
|
||||||
AuthorizationHeaderBearerToken: "Bearer token",
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
ExpectedCode: 200,
|
ExpectedCode: 200,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "good-token-success-true-with-ignored-error-because-success-true",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=true&error=failed",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 200,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "good-token-success-false",
|
Name: "good-token-success-false",
|
||||||
Path: "/api/v1/endpoints/g_n/external?success=false",
|
Path: "/api/v1/endpoints/g_n/external?success=false",
|
||||||
@ -82,6 +94,12 @@ func TestCreateExternalEndpointResult(t *testing.T) {
|
|||||||
AuthorizationHeaderBearerToken: "Bearer token",
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
ExpectedCode: 200,
|
ExpectedCode: 200,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "good-token-success-false-with-error",
|
||||||
|
Path: "/api/v1/endpoints/g_n/external?success=false&error=failed",
|
||||||
|
AuthorizationHeaderBearerToken: "Bearer token",
|
||||||
|
ExpectedCode: 200,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
@ -108,21 +126,33 @@ func TestCreateExternalEndpointResult(t *testing.T) {
|
|||||||
if endpointStatus.Key != "g_n" {
|
if endpointStatus.Key != "g_n" {
|
||||||
t.Errorf("expected key to be g_n but got %s", endpointStatus.Key)
|
t.Errorf("expected key to be g_n but got %s", endpointStatus.Key)
|
||||||
}
|
}
|
||||||
if len(endpointStatus.Results) != 3 {
|
if len(endpointStatus.Results) != 5 {
|
||||||
t.Errorf("expected 3 results but got %d", len(endpointStatus.Results))
|
t.Errorf("expected 3 results but got %d", len(endpointStatus.Results))
|
||||||
}
|
}
|
||||||
if !endpointStatus.Results[0].Success {
|
if !endpointStatus.Results[0].Success {
|
||||||
t.Errorf("expected first result to be successful")
|
t.Errorf("expected first result to be successful")
|
||||||
}
|
}
|
||||||
if endpointStatus.Results[1].Success {
|
if !endpointStatus.Results[1].Success {
|
||||||
t.Errorf("expected second result to be unsuccessful")
|
t.Errorf("expected second result to be successful")
|
||||||
|
}
|
||||||
|
if len(endpointStatus.Results[1].Errors) > 0 {
|
||||||
|
t.Errorf("expected second result to have no errors")
|
||||||
}
|
}
|
||||||
if endpointStatus.Results[2].Success {
|
if endpointStatus.Results[2].Success {
|
||||||
t.Errorf("expected third result to be unsuccessful")
|
t.Errorf("expected third result to be unsuccessful")
|
||||||
}
|
}
|
||||||
|
if endpointStatus.Results[3].Success {
|
||||||
|
t.Errorf("expected fourth result to be unsuccessful")
|
||||||
|
}
|
||||||
|
if endpointStatus.Results[4].Success {
|
||||||
|
t.Errorf("expected fifth result to be unsuccessful")
|
||||||
|
}
|
||||||
|
if len(endpointStatus.Results[4].Errors) == 0 || endpointStatus.Results[4].Errors[0] != "failed" {
|
||||||
|
t.Errorf("expected fifth result to have errors: failed")
|
||||||
|
}
|
||||||
externalEndpointFromConfig := cfg.GetExternalEndpointByKey("g_n")
|
externalEndpointFromConfig := cfg.GetExternalEndpointByKey("g_n")
|
||||||
if externalEndpointFromConfig.NumberOfFailuresInARow != 2 {
|
if externalEndpointFromConfig.NumberOfFailuresInARow != 3 {
|
||||||
t.Errorf("expected 2 failures in a row but got %d", externalEndpointFromConfig.NumberOfFailuresInARow)
|
t.Errorf("expected 3 failures in a row but got %d", externalEndpointFromConfig.NumberOfFailuresInARow)
|
||||||
}
|
}
|
||||||
if externalEndpointFromConfig.NumberOfSuccessesInARow != 0 {
|
if externalEndpointFromConfig.NumberOfSuccessesInARow != 0 {
|
||||||
t.Errorf("expected 0 successes in a row but got %d", externalEndpointFromConfig.NumberOfSuccessesInARow)
|
t.Errorf("expected 0 successes in a row but got %d", externalEndpointFromConfig.NumberOfSuccessesInARow)
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/config"
|
"github.com/TwiN/gatus/v5/config"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/ui"
|
"github.com/TwiN/gatus/v5/config/ui"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage/store"
|
"github.com/TwiN/gatus/v5/storage/store"
|
||||||
"github.com/TwiN/gatus/v5/watchdog"
|
"github.com/TwiN/gatus/v5/watchdog"
|
||||||
)
|
)
|
||||||
@ -20,7 +20,7 @@ func TestSinglePageApplication(t *testing.T) {
|
|||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
Group: "core",
|
Group: "core",
|
||||||
@ -34,8 +34,8 @@ func TestSinglePageApplication(t *testing.T) {
|
|||||||
Title: "example-title",
|
Title: "example-title",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
api := New(cfg)
|
api := New(cfg)
|
||||||
router := api.Router()
|
router := api.Router()
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
|
@ -16,11 +16,16 @@ import (
|
|||||||
"github.com/TwiN/gocache/v2"
|
"github.com/TwiN/gocache/v2"
|
||||||
"github.com/TwiN/whois"
|
"github.com/TwiN/whois"
|
||||||
"github.com/ishidawataru/sctp"
|
"github.com/ishidawataru/sctp"
|
||||||
|
"github.com/miekg/dns"
|
||||||
ping "github.com/prometheus-community/pro-bing"
|
ping "github.com/prometheus-community/pro-bing"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsPort = 53
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// injectedHTTPClient is used for testing purposes
|
// injectedHTTPClient is used for testing purposes
|
||||||
injectedHTTPClient *http.Client
|
injectedHTTPClient *http.Client
|
||||||
@ -291,6 +296,49 @@ func QueryWebSocket(address, body string, config *Config) (bool, []byte, error)
|
|||||||
return true, msg[:n], nil
|
return true, msg[:n], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string, body []byte, err error) {
|
||||||
|
if !strings.Contains(url, ":") {
|
||||||
|
url = fmt.Sprintf("%s:%d", url, dnsPort)
|
||||||
|
}
|
||||||
|
queryTypeAsUint16 := dns.StringToType[queryType]
|
||||||
|
c := new(dns.Client)
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(queryName, queryTypeAsUint16)
|
||||||
|
r, _, err := c.Exchange(m, url)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", nil, err
|
||||||
|
}
|
||||||
|
connected = true
|
||||||
|
dnsRcode = dns.RcodeToString[r.Rcode]
|
||||||
|
for _, rr := range r.Answer {
|
||||||
|
switch rr.Header().Rrtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
if a, ok := rr.(*dns.A); ok {
|
||||||
|
body = []byte(a.A.String())
|
||||||
|
}
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||||
|
body = []byte(aaaa.AAAA.String())
|
||||||
|
}
|
||||||
|
case dns.TypeCNAME:
|
||||||
|
if cname, ok := rr.(*dns.CNAME); ok {
|
||||||
|
body = []byte(cname.Target)
|
||||||
|
}
|
||||||
|
case dns.TypeMX:
|
||||||
|
if mx, ok := rr.(*dns.MX); ok {
|
||||||
|
body = []byte(mx.Mx)
|
||||||
|
}
|
||||||
|
case dns.TypeNS:
|
||||||
|
if ns, ok := rr.(*dns.NS); ok {
|
||||||
|
body = []byte(ns.Ns)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
body = []byte("query type is not supported yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connected, dnsRcode, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
||||||
func InjectHTTPClient(httpClient *http.Client) {
|
func InjectHTTPClient(httpClient *http.Client) {
|
||||||
injectedHTTPClient = httpClient
|
injectedHTTPClient = httpClient
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
|
"github.com/TwiN/gatus/v5/pattern"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -334,3 +336,97 @@ func TestTlsRenegotiation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQueryDNS(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputDNS dns.Config
|
||||||
|
inputURL string
|
||||||
|
expectedDNSCode string
|
||||||
|
expectedBody string
|
||||||
|
isErrExpected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test Config with type A",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "93.184.215.14",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type AAAA",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "AAAA",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "2606:2800:21f:cb07:6820:80da:af6b:8b2c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type CNAME",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "CNAME",
|
||||||
|
QueryName: "en.wikipedia.org.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "dyna.wikimedia.org.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type MX",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "MX",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: ".",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type NS",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "NS",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "*.iana-servers.net.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with fake type and retrieve error",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "B",
|
||||||
|
QueryName: "example",
|
||||||
|
},
|
||||||
|
inputURL: "8.8.8.8",
|
||||||
|
isErrExpected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, dnsRCode, body, err := QueryDNS(test.inputDNS.QueryType, test.inputDNS.QueryName, test.inputURL)
|
||||||
|
if test.isErrExpected && err == nil {
|
||||||
|
t.Errorf("there should be an error")
|
||||||
|
}
|
||||||
|
if dnsRCode != test.expectedDNSCode {
|
||||||
|
t.Errorf("expected DNSRCode to be %s, got %s", test.expectedDNSCode, dnsRCode)
|
||||||
|
}
|
||||||
|
if test.inputDNS.QueryType == "NS" {
|
||||||
|
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
||||||
|
if !pattern.Match(test.expectedBody, string(body)) {
|
||||||
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if string(body) != test.expectedBody {
|
||||||
|
t.Errorf("got %s, expected result %s,", string(body), test.expectedBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,14 +15,13 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||||
"github.com/TwiN/gatus/v5/config/remote"
|
"github.com/TwiN/gatus/v5/config/remote"
|
||||||
"github.com/TwiN/gatus/v5/config/ui"
|
"github.com/TwiN/gatus/v5/config/ui"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/security"
|
"github.com/TwiN/gatus/v5/security"
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,10 +73,10 @@ type Config struct {
|
|||||||
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
||||||
|
|
||||||
// Endpoints is the list of endpoints to monitor
|
// Endpoints is the list of endpoints to monitor
|
||||||
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"`
|
Endpoints []*endpoint.Endpoint `yaml:"endpoints,omitempty"`
|
||||||
|
|
||||||
// ExternalEndpoints is the list of all external endpoints
|
// ExternalEndpoints is the list of all external endpoints
|
||||||
ExternalEndpoints []*core.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
ExternalEndpoints []*endpoint.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
||||||
|
|
||||||
// Storage is the configuration for how the data is stored
|
// Storage is the configuration for how the data is stored
|
||||||
Storage *storage.Config `yaml:"storage,omitempty"`
|
Storage *storage.Config `yaml:"storage,omitempty"`
|
||||||
@ -102,20 +101,20 @@ type Config struct {
|
|||||||
lastFileModTime time.Time // last modification time
|
lastFileModTime time.Time // last modification time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
|
func (config *Config) GetEndpointByKey(key string) *endpoint.Endpoint {
|
||||||
for i := 0; i < len(config.Endpoints); i++ {
|
for i := 0; i < len(config.Endpoints); i++ {
|
||||||
ep := config.Endpoints[i]
|
ep := config.Endpoints[i]
|
||||||
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
|
if ep.Key() == key {
|
||||||
return ep
|
return ep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) GetExternalEndpointByKey(key string) *core.ExternalEndpoint {
|
func (config *Config) GetExternalEndpointByKey(key string) *endpoint.ExternalEndpoint {
|
||||||
for i := 0; i < len(config.ExternalEndpoints); i++ {
|
for i := 0; i < len(config.ExternalEndpoints); i++ {
|
||||||
ee := config.ExternalEndpoints[i]
|
ee := config.ExternalEndpoints[i]
|
||||||
if util.ConvertGroupAndEndpointNameToKey(ee.Group, ee.Name) == key {
|
if ee.Key() == key {
|
||||||
return ee
|
return ee
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +188,7 @@ func LoadConfiguration(configPath string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("error reading configuration from directory %s: %w", usedConfigPath, err)
|
return nil, fmt.Errorf("error reading configuration from directory %s: %w", usedConfigPath, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[config.LoadConfiguration] Reading configuration from configFile=%s", configPath)
|
log.Printf("[config.LoadConfiguration] Reading configuration from configFile=%s", usedConfigPath)
|
||||||
if data, err := os.ReadFile(usedConfigPath); err != nil {
|
if data, err := os.ReadFile(usedConfigPath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
@ -246,16 +245,13 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
if config == nil || config.Endpoints == nil || len(config.Endpoints) == 0 {
|
if config == nil || config.Endpoints == nil || len(config.Endpoints) == 0 {
|
||||||
err = ErrNoEndpointInConfig
|
err = ErrNoEndpointInConfig
|
||||||
} else {
|
} else {
|
||||||
validateAlertingConfig(config.Alerting, config.Endpoints, config.Debug)
|
validateAlertingConfig(config.Alerting, config.Endpoints, config.ExternalEndpoints, config.Debug)
|
||||||
if err := validateSecurityConfig(config); err != nil {
|
if err := validateSecurityConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := validateEndpointsConfig(config); err != nil {
|
if err := validateEndpointsConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := validateExternalEndpointsConfig(config); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := validateWebConfig(config); err != nil {
|
if err := validateWebConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -339,28 +335,37 @@ func validateWebConfig(config *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointsConfig(config *Config) error {
|
func validateEndpointsConfig(config *Config) error {
|
||||||
for _, endpoint := range config.Endpoints {
|
duplicateValidationMap := make(map[string]bool)
|
||||||
|
// Validate endpoints
|
||||||
|
for _, ep := range config.Endpoints {
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name)
|
log.Printf("[config.validateEndpointsConfig] Validating endpoint '%s'", ep.Name)
|
||||||
}
|
}
|
||||||
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
if endpointKey := ep.Key(); duplicateValidationMap[endpointKey] {
|
||||||
return fmt.Errorf("invalid endpoint %s: %w", endpoint.DisplayName(), err)
|
return fmt.Errorf("invalid endpoint %s: name and group combination must be unique", ep.Key())
|
||||||
|
} else {
|
||||||
|
duplicateValidationMap[endpointKey] = true
|
||||||
|
}
|
||||||
|
if err := ep.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return fmt.Errorf("invalid endpoint %s: %w", ep.Key(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
log.Printf("[config.validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
|
||||||
return nil
|
// Validate external endpoints
|
||||||
}
|
for _, ee := range config.ExternalEndpoints {
|
||||||
|
|
||||||
func validateExternalEndpointsConfig(config *Config) error {
|
|
||||||
for _, externalEndpoint := range config.ExternalEndpoints {
|
|
||||||
if config.Debug {
|
if config.Debug {
|
||||||
log.Printf("[config.validateExternalEndpointsConfig] Validating external endpoint '%s'", externalEndpoint.Name)
|
log.Printf("[config.validateEndpointsConfig] Validating external endpoint '%s'", ee.Name)
|
||||||
}
|
}
|
||||||
if err := externalEndpoint.ValidateAndSetDefaults(); err != nil {
|
if endpointKey := ee.Key(); duplicateValidationMap[endpointKey] {
|
||||||
return fmt.Errorf("invalid external endpoint %s: %w", externalEndpoint.DisplayName(), err)
|
return fmt.Errorf("invalid external endpoint %s: name and group combination must be unique", ee.Key())
|
||||||
|
} else {
|
||||||
|
duplicateValidationMap[endpointKey] = true
|
||||||
|
}
|
||||||
|
if err := ee.ValidateAndSetDefaults(); err != nil {
|
||||||
|
return fmt.Errorf("invalid external endpoint %s: %w", ee.Key(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[config.validateExternalEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
|
log.Printf("[config.validateEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,9 +386,9 @@ func validateSecurityConfig(config *Config) error {
|
|||||||
|
|
||||||
// validateAlertingConfig validates the alerting configuration
|
// validateAlertingConfig validates the alerting configuration
|
||||||
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
|
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
|
||||||
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Endpoint.ValidateAndSetDefaults()
|
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before endpoint.Endpoint.ValidateAndSetDefaults()
|
||||||
// sets the default alert values when none are set.
|
// sets the default alert values when none are set.
|
||||||
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.Endpoint, debug bool) {
|
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoint.Endpoint, externalEndpoints []*endpoint.ExternalEndpoint, debug bool) {
|
||||||
if alertingConfig == nil {
|
if alertingConfig == nil {
|
||||||
log.Printf("[config.validateAlertingConfig] Alerting is not configured")
|
log.Printf("[config.validateAlertingConfig] Alerting is not configured")
|
||||||
return
|
return
|
||||||
@ -392,12 +397,13 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
alert.TypeAWSSES,
|
alert.TypeAWSSES,
|
||||||
alert.TypeCustom,
|
alert.TypeCustom,
|
||||||
alert.TypeDiscord,
|
alert.TypeDiscord,
|
||||||
|
alert.TypeEmail,
|
||||||
alert.TypeGitHub,
|
alert.TypeGitHub,
|
||||||
alert.TypeGitLab,
|
alert.TypeGitLab,
|
||||||
|
alert.TypeGitea,
|
||||||
alert.TypeGoogleChat,
|
alert.TypeGoogleChat,
|
||||||
alert.TypeGotify,
|
alert.TypeGotify,
|
||||||
alert.TypeJetBrainsSpace,
|
alert.TypeJetBrainsSpace,
|
||||||
alert.TypeEmail,
|
|
||||||
alert.TypeMatrix,
|
alert.TypeMatrix,
|
||||||
alert.TypeMattermost,
|
alert.TypeMattermost,
|
||||||
alert.TypeMessagebird,
|
alert.TypeMessagebird,
|
||||||
@ -409,6 +415,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
alert.TypeTeams,
|
alert.TypeTeams,
|
||||||
alert.TypeTelegram,
|
alert.TypeTelegram,
|
||||||
alert.TypeTwilio,
|
alert.TypeTwilio,
|
||||||
|
alert.TypeZulip,
|
||||||
}
|
}
|
||||||
var validProviders, invalidProviders []alert.Type
|
var validProviders, invalidProviders []alert.Type
|
||||||
for _, alertType := range alertTypes {
|
for _, alertType := range alertTypes {
|
||||||
@ -417,11 +424,21 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
if alertProvider.IsValid() {
|
if alertProvider.IsValid() {
|
||||||
// Parse alerts with the provider's default alert
|
// Parse alerts with the provider's default alert
|
||||||
if alertProvider.GetDefaultAlert() != nil {
|
if alertProvider.GetDefaultAlert() != nil {
|
||||||
for _, endpoint := range endpoints {
|
for _, ep := range endpoints {
|
||||||
for alertIndex, endpointAlert := range endpoint.Alerts {
|
for alertIndex, endpointAlert := range ep.Alerts {
|
||||||
if alertType == endpointAlert.Type {
|
if alertType == endpointAlert.Type {
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("[config.validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, endpoint.Name)
|
log.Printf("[config.validateAlertingConfig] Parsing alert %d with default alert for provider=%s in endpoint with key=%s", alertIndex, alertType, ep.Key())
|
||||||
|
}
|
||||||
|
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ee := range externalEndpoints {
|
||||||
|
for alertIndex, endpointAlert := range ee.Alerts {
|
||||||
|
if alertType == endpointAlert.Type {
|
||||||
|
if debug {
|
||||||
|
log.Printf("[config.validateAlertingConfig] Parsing alert %d with default alert for provider=%s in endpoint with key=%s", alertIndex, alertType, ee.Key())
|
||||||
}
|
}
|
||||||
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
|
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||||
@ -29,13 +30,14 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
"github.com/TwiN/gatus/v5/alerting/provider/telegram"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
"github.com/TwiN/gatus/v5/alerting/provider/twilio"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||||
"github.com/TwiN/gatus/v5/config/web"
|
"github.com/TwiN/gatus/v5/config/web"
|
||||||
"github.com/TwiN/gatus/v5/core"
|
|
||||||
"github.com/TwiN/gatus/v5/storage"
|
"github.com/TwiN/gatus/v5/storage"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadConfiguration(t *testing.T) {
|
func TestLoadConfiguration(t *testing.T) {
|
||||||
|
yes := true
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
@ -65,7 +67,7 @@ func TestLoadConfiguration(t *testing.T) {
|
|||||||
endpoints:
|
endpoints:
|
||||||
- name: website`,
|
- name: website`,
|
||||||
},
|
},
|
||||||
expectedError: core.ErrEndpointWithNoURL,
|
expectedError: endpoint.ErrEndpointWithNoURL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config-file-with-endpoint-that-has-no-conditions",
|
name: "config-file-with-endpoint-that-has-no-conditions",
|
||||||
@ -76,7 +78,7 @@ endpoints:
|
|||||||
- name: website
|
- name: website
|
||||||
url: https://twin.sh/health`,
|
url: https://twin.sh/health`,
|
||||||
},
|
},
|
||||||
expectedError: core.ErrEndpointWithNoCondition,
|
expectedError: endpoint.ErrEndpointWithNoCondition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "config-file",
|
name: "config-file",
|
||||||
@ -90,11 +92,11 @@ endpoints:
|
|||||||
- "[STATUS] == 200"`,
|
- "[STATUS] == 200"`,
|
||||||
},
|
},
|
||||||
expectedConfig: &Config{
|
expectedConfig: &Config{
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "website",
|
Name: "website",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -136,21 +138,21 @@ endpoints:
|
|||||||
- "[BODY].status == UP"`,
|
- "[BODY].status == UP"`,
|
||||||
},
|
},
|
||||||
expectedConfig: &Config{
|
expectedConfig: &Config{
|
||||||
Endpoints: []*core.Endpoint{
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "one",
|
Name: "one",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Conditions: []core.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[CONNECTED] == true", "[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "two",
|
Name: "two",
|
||||||
URL: "https://example.org",
|
URL: "https://example.org",
|
||||||
Conditions: []core.Condition{"len([BODY]) > 0"},
|
Conditions: []endpoint.Condition{"len([BODY]) > 0"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "three",
|
Name: "three",
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200", "[BODY].status == UP"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -165,6 +167,8 @@ metrics: true
|
|||||||
alerting:
|
alerting:
|
||||||
slack:
|
slack:
|
||||||
webhook-url: https://hooks.slack.com/services/xxx/yyy/zzz
|
webhook-url: https://hooks.slack.com/services/xxx/yyy/zzz
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: example
|
- name: example
|
||||||
@ -179,6 +183,12 @@ alerting:
|
|||||||
discord:
|
discord:
|
||||||
webhook-url: https://discord.com/api/webhooks/xxx/yyy
|
webhook-url: https://discord.com/api/webhooks/xxx/yyy
|
||||||
|
|
||||||
|
external-endpoints:
|
||||||
|
- name: ext-ep-test
|
||||||
|
token: "potato"
|
||||||
|
alerts:
|
||||||
|
- type: slack
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: frontend
|
- name: frontend
|
||||||
url: https://example.com
|
url: https://example.com
|
||||||
@ -190,19 +200,32 @@ endpoints:
|
|||||||
Metrics: true,
|
Metrics: true,
|
||||||
Alerting: &alerting.Config{
|
Alerting: &alerting.Config{
|
||||||
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
|
Discord: &discord.AlertProvider{WebhookURL: "https://discord.com/api/webhooks/xxx/yyy"},
|
||||||
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz"},
|
Slack: &slack.AlertProvider{WebhookURL: "https://hooks.slack.com/services/xxx/yyy/zzz", DefaultAlert: &alert.Alert{Enabled: &yes}},
|
||||||
},
|
},
|
||||||
Endpoints: []*core.Endpoint{
|
ExternalEndpoints: []*endpoint.ExternalEndpoint{
|
||||||
|
{
|
||||||
|
Name: "ext-ep-test",
|
||||||
|
Token: "potato",
|
||||||
|
Alerts: []*alert.Alert{
|
||||||
|
{
|
||||||
|
Type: alert.TypeSlack,
|
||||||
|
FailureThreshold: 3,
|
||||||
|
SuccessThreshold: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Endpoints: []*endpoint.Endpoint{
|
||||||
{
|
{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "https://example.org",
|
URL: "https://example.org",
|
||||||
Interval: 5 * time.Second,
|
Interval: 5 * time.Second,
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "frontend",
|
Name: "frontend",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Conditions: []core.Condition{"[STATUS] == 200"},
|
Conditions: []endpoint.Condition{"[STATUS] == 200"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -325,10 +348,6 @@ external-endpoints:
|
|||||||
- name: ext-ep-test
|
- name: ext-ep-test
|
||||||
group: core
|
group: core
|
||||||
token: "potato"
|
token: "potato"
|
||||||
alerts:
|
|
||||||
- type: discord
|
|
||||||
description: "healthcheck failed"
|
|
||||||
send-on-resolved: true
|
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@ -382,18 +401,7 @@ endpoints:
|
|||||||
if config.ExternalEndpoints[0].Token != "potato" {
|
if config.ExternalEndpoints[0].Token != "potato" {
|
||||||
t.Errorf("Token should have been %s", "potato")
|
t.Errorf("Token should have been %s", "potato")
|
||||||
}
|
}
|
||||||
if len(config.ExternalEndpoints[0].Alerts) != 1 {
|
|
||||||
t.Error("Should have returned one alert")
|
|
||||||
}
|
|
||||||
if config.ExternalEndpoints[0].Alerts[0].Type != alert.TypeDiscord {
|
|
||||||
t.Errorf("Type should have been %s", alert.TypeDiscord)
|
|
||||||
}
|
|
||||||
if config.ExternalEndpoints[0].Alerts[0].FailureThreshold != 3 {
|
|
||||||
t.Errorf("FailureThreshold should have been %d, got %d", 3, config.ExternalEndpoints[0].Alerts[0].FailureThreshold)
|
|
||||||
}
|
|
||||||
if config.ExternalEndpoints[0].Alerts[0].SuccessThreshold != 2 {
|
|
||||||
t.Errorf("SuccessThreshold should have been %d, got %d", 2, config.ExternalEndpoints[0].Alerts[0].SuccessThreshold)
|
|
||||||
}
|
|
||||||
if len(config.Endpoints) != 3 {
|
if len(config.Endpoints) != 3 {
|
||||||
t.Error("Should have returned two endpoints")
|
t.Error("Should have returned two endpoints")
|
||||||
}
|
}
|
||||||
@ -439,7 +447,6 @@ endpoints:
|
|||||||
if len(config.Endpoints[1].Conditions) != 2 {
|
if len(config.Endpoints[1].Conditions) != 2 {
|
||||||
t.Errorf("There should have been %d conditions", 2)
|
t.Errorf("There should have been %d conditions", 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[2].URL != "https://example.com/" {
|
if config.Endpoints[2].URL != "https://example.com/" {
|
||||||
t.Errorf("URL should have been %s", "https://example.com/")
|
t.Errorf("URL should have been %s", "https://example.com/")
|
||||||
}
|
}
|
||||||
@ -689,8 +696,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != core.GatusUserAgent {
|
if userAgent := config.Endpoints[0].Headers["User-Agent"]; userAgent != endpoint.GatusUserAgent {
|
||||||
t.Errorf("User-Agent should've been %s because it's the default value, got %s", core.GatusUserAgent, userAgent)
|
t.Errorf("User-Agent should've been %s because it's the default value, got %s", endpoint.GatusUserAgent, userAgent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -924,7 +931,7 @@ alerting:
|
|||||||
default-alert:
|
default-alert:
|
||||||
enabled: true
|
enabled: true
|
||||||
failure-threshold: 10
|
failure-threshold: 10
|
||||||
success-threshold: 1
|
success-threshold: 15
|
||||||
pagerduty:
|
pagerduty:
|
||||||
integration-key: "00000000000000000000000000000000"
|
integration-key: "00000000000000000000000000000000"
|
||||||
default-alert:
|
default-alert:
|
||||||
@ -977,6 +984,29 @@ alerting:
|
|||||||
enabled: true
|
enabled: true
|
||||||
failure-threshold: 5
|
failure-threshold: 5
|
||||||
success-threshold: 3
|
success-threshold: 3
|
||||||
|
email:
|
||||||
|
from: "from@example.com"
|
||||||
|
username: "from@example.com"
|
||||||
|
password: "hunter2"
|
||||||
|
host: "mail.example.com"
|
||||||
|
port: 587
|
||||||
|
to: "recipient1@example.com,recipient2@example.com"
|
||||||
|
client:
|
||||||
|
insecure: false
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
gotify:
|
||||||
|
server-url: "https://gotify.example"
|
||||||
|
token: "**************"
|
||||||
|
default-alert:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
external-endpoints:
|
||||||
|
- name: ext-ep-test
|
||||||
|
group: core
|
||||||
|
token: potato
|
||||||
|
alerts:
|
||||||
|
- type: discord
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
- name: website
|
- name: website
|
||||||
@ -987,12 +1017,14 @@ endpoints:
|
|||||||
- type: mattermost
|
- type: mattermost
|
||||||
- type: messagebird
|
- type: messagebird
|
||||||
- type: discord
|
- type: discord
|
||||||
success-threshold: 2 # test endpoint alert override
|
success-threshold: 8 # test endpoint alert override
|
||||||
- type: telegram
|
- type: telegram
|
||||||
- type: twilio
|
- type: twilio
|
||||||
- type: teams
|
- type: teams
|
||||||
- type: pushover
|
- type: pushover
|
||||||
- type: jetbrainsspace
|
- type: jetbrainsspace
|
||||||
|
- type: email
|
||||||
|
- type: gotify
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
`))
|
`))
|
||||||
@ -1071,6 +1103,12 @@ endpoints:
|
|||||||
if config.Alerting.Discord.GetDefaultAlert() == nil {
|
if config.Alerting.Discord.GetDefaultAlert() == nil {
|
||||||
t.Fatal("Discord.GetDefaultAlert() shouldn't have returned nil")
|
t.Fatal("Discord.GetDefaultAlert() shouldn't have returned nil")
|
||||||
}
|
}
|
||||||
|
if config.Alerting.Discord.GetDefaultAlert().FailureThreshold != 10 {
|
||||||
|
t.Errorf("Discord default alert failure threshold should've been %d, but was %d", 10, config.Alerting.Discord.GetDefaultAlert().FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Alerting.Discord.GetDefaultAlert().SuccessThreshold != 15 {
|
||||||
|
t.Errorf("Discord default alert success threshold should've been %d, but was %d", 15, config.Alerting.Discord.GetDefaultAlert().SuccessThreshold)
|
||||||
|
}
|
||||||
if config.Alerting.Discord.WebhookURL != "http://example.org" {
|
if config.Alerting.Discord.WebhookURL != "http://example.org" {
|
||||||
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
|
t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
|
||||||
}
|
}
|
||||||
@ -1107,6 +1145,7 @@ endpoints:
|
|||||||
if config.Alerting.JetBrainsSpace == nil || !config.Alerting.JetBrainsSpace.IsValid() {
|
if config.Alerting.JetBrainsSpace == nil || !config.Alerting.JetBrainsSpace.IsValid() {
|
||||||
t.Fatal("JetBrainsSpace alerting config should've been valid")
|
t.Fatal("JetBrainsSpace alerting config should've been valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Alerting.JetBrainsSpace.GetDefaultAlert() == nil {
|
if config.Alerting.JetBrainsSpace.GetDefaultAlert() == nil {
|
||||||
t.Fatal("JetBrainsSpace.GetDefaultAlert() shouldn't have returned nil")
|
t.Fatal("JetBrainsSpace.GetDefaultAlert() shouldn't have returned nil")
|
||||||
}
|
}
|
||||||
@ -1120,6 +1159,67 @@ endpoints:
|
|||||||
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "baz", config.Alerting.JetBrainsSpace.Token)
|
t.Errorf("JetBrainsSpace webhook should've been %s, but was %s", "baz", config.Alerting.JetBrainsSpace.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Alerting.Email == nil || !config.Alerting.Email.IsValid() {
|
||||||
|
t.Fatal("Email alerting config should've been valid")
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.GetDefaultAlert() == nil {
|
||||||
|
t.Fatal("Email.GetDefaultAlert() shouldn't have returned nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.From != "from@example.com" {
|
||||||
|
t.Errorf("Email from should've been %s, but was %s", "from@example.com", config.Alerting.Email.From)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Username != "from@example.com" {
|
||||||
|
t.Errorf("Email username should've been %s, but was %s", "from@example.com", config.Alerting.Email.Username)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Password != "hunter2" {
|
||||||
|
t.Errorf("Email password should've been %s, but was %s", "hunter2", config.Alerting.Email.Password)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Host != "mail.example.com" {
|
||||||
|
t.Errorf("Email host should've been %s, but was %s", "mail.example.com", config.Alerting.Email.Host)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.Port != 587 {
|
||||||
|
t.Errorf("Email port should've been %d, but was %d", 587, config.Alerting.Email.Port)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.To != "recipient1@example.com,recipient2@example.com" {
|
||||||
|
t.Errorf("Email to should've been %s, but was %s", "recipient1@example.com,recipient2@example.com", config.Alerting.Email.To)
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.ClientConfig == nil {
|
||||||
|
t.Fatal("Email client config should've been set")
|
||||||
|
}
|
||||||
|
if config.Alerting.Email.ClientConfig.Insecure {
|
||||||
|
t.Error("Email client config should've been secure")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Alerting.Gotify == nil || !config.Alerting.Gotify.IsValid() {
|
||||||
|
t.Fatal("Gotify alerting config should've been valid")
|
||||||
|
}
|
||||||
|
if config.Alerting.Gotify.GetDefaultAlert() == nil {
|
||||||
|
t.Fatal("Gotify.GetDefaultAlert() shouldn't have returned nil")
|
||||||
|
}
|
||||||
|
if config.Alerting.Gotify.ServerURL != "https://gotify.example" {
|
||||||
|
t.Errorf("Gotify server URL should've been %s, but was %s", "https://gotify.example", config.Alerting.Gotify.ServerURL)
|
||||||
|
}
|
||||||
|
if config.Alerting.Gotify.Token != "**************" {
|
||||||
|
t.Errorf("Gotify token should've been %s, but was %s", "**************", config.Alerting.Gotify.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// External endpoints
|
||||||
|
if len(config.ExternalEndpoints) != 1 {
|
||||||
|
t.Error("There should've been 1 external endpoint")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Alerts[0].Type != alert.TypeDiscord {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeDiscord, config.ExternalEndpoints[0].Alerts[0].Type)
|
||||||
|
}
|
||||||
|
if !config.ExternalEndpoints[0].Alerts[0].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Alerts[0].FailureThreshold != 10 {
|
||||||
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.ExternalEndpoints[0].Alerts[0].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.ExternalEndpoints[0].Alerts[0].SuccessThreshold != 15 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.ExternalEndpoints[0].Alerts[0].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
// Endpoints
|
// Endpoints
|
||||||
if len(config.Endpoints) != 1 {
|
if len(config.Endpoints) != 1 {
|
||||||
t.Error("There should've been 1 endpoint")
|
t.Error("There should've been 1 endpoint")
|
||||||
@ -1130,8 +1230,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Interval != 60*time.Second {
|
if config.Endpoints[0].Interval != 60*time.Second {
|
||||||
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
|
||||||
}
|
}
|
||||||
if len(config.Endpoints[0].Alerts) != 10 {
|
if len(config.Endpoints[0].Alerts) != 12 {
|
||||||
t.Fatal("There should've been 10 alerts configured")
|
t.Fatalf("There should've been 12 alerts configured, got %d", len(config.Endpoints[0].Alerts))
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
if config.Endpoints[0].Alerts[0].Type != alert.TypeSlack {
|
||||||
@ -1192,8 +1292,8 @@ endpoints:
|
|||||||
if config.Endpoints[0].Alerts[4].FailureThreshold != 10 {
|
if config.Endpoints[0].Alerts[4].FailureThreshold != 10 {
|
||||||
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[4].FailureThreshold)
|
t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Endpoints[0].Alerts[4].FailureThreshold)
|
||||||
}
|
}
|
||||||
if config.Endpoints[0].Alerts[4].SuccessThreshold != 2 {
|
if config.Endpoints[0].Alerts[4].SuccessThreshold != 8 {
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[4].SuccessThreshold)
|
t.Errorf("The default success threshold of the alert should've been %d because it was explicitly overriden, but it was %d", 8, config.Endpoints[0].Alerts[4].SuccessThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Endpoints[0].Alerts[5].Type != alert.TypeTelegram {
|
if config.Endpoints[0].Alerts[5].Type != alert.TypeTelegram {
|
||||||
@ -1255,10 +1355,36 @@ endpoints:
|
|||||||
t.Error("The alert should've been enabled")
|
t.Error("The alert should've been enabled")
|
||||||
}
|
}
|
||||||
if config.Endpoints[0].Alerts[9].FailureThreshold != 5 {
|
if config.Endpoints[0].Alerts[9].FailureThreshold != 5 {
|
||||||
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].FailureThreshold)
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 5, config.Endpoints[0].Alerts[9].FailureThreshold)
|
||||||
}
|
}
|
||||||
if config.Endpoints[0].Alerts[9].SuccessThreshold != 3 {
|
if config.Endpoints[0].Alerts[9].SuccessThreshold != 3 {
|
||||||
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[9].SuccessThreshold)
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[9].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[10].Type != alert.TypeEmail {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeEmail, config.Endpoints[0].Alerts[10].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[10].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[10].FailureThreshold != 3 {
|
||||||
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[10].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[10].SuccessThreshold != 2 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[10].SuccessThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoints[0].Alerts[11].Type != alert.TypeGotify {
|
||||||
|
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeGotify, config.Endpoints[0].Alerts[11].Type)
|
||||||
|
}
|
||||||
|
if !config.Endpoints[0].Alerts[11].IsEnabled() {
|
||||||
|
t.Error("The alert should've been enabled")
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[11].FailureThreshold != 3 {
|
||||||
|
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Endpoints[0].Alerts[11].FailureThreshold)
|
||||||
|
}
|
||||||
|
if config.Endpoints[0].Alerts[11].SuccessThreshold != 2 {
|
||||||
|
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Endpoints[0].Alerts[11].SuccessThreshold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1532,6 +1658,99 @@ endpoints:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAndValidateConfigBytesWithDuplicateEndpointName(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
shouldError bool
|
||||||
|
config string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "same-name-no-group",
|
||||||
|
shouldError: true,
|
||||||
|
config: `
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-different-group",
|
||||||
|
shouldError: false,
|
||||||
|
config: `
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- name: ep1
|
||||||
|
group: g1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-same-group",
|
||||||
|
shouldError: true,
|
||||||
|
config: `
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
group: g1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- name: ep1
|
||||||
|
group: g1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-different-endpoint-type",
|
||||||
|
shouldError: true,
|
||||||
|
config: `
|
||||||
|
external-endpoints:
|
||||||
|
- name: ep1
|
||||||
|
token: "12345678"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-name-different-group-different-endpoint-type",
|
||||||
|
shouldError: false,
|
||||||
|
config: `
|
||||||
|
external-endpoints:
|
||||||
|
- name: ep1
|
||||||
|
group: gr1
|
||||||
|
token: "12345678"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: ep1
|
||||||
|
url: https://twin.sh/health
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
||||||
|
if scenario.shouldError && err == nil {
|
||||||
|
t.Error("should've returned an error")
|
||||||
|
} else if !scenario.shouldError && err != nil {
|
||||||
|
t.Error("shouldn't have returned an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithInvalidStorageConfig(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithInvalidStorageConfig(t *testing.T) {
|
||||||
_, err := parseAndValidateConfigBytes([]byte(`
|
_, err := parseAndValidateConfigBytes([]byte(`
|
||||||
storage:
|
storage:
|
||||||
@ -1645,7 +1864,7 @@ endpoints:
|
|||||||
|
|
||||||
func TestParseAndValidateConfigBytesWithNoEndpoints(t *testing.T) {
|
func TestParseAndValidateConfigBytesWithNoEndpoints(t *testing.T) {
|
||||||
_, err := parseAndValidateConfigBytes([]byte(``))
|
_, err := parseAndValidateConfigBytes([]byte(``))
|
||||||
if err != ErrNoEndpointInConfig {
|
if !errors.Is(err, ErrNoEndpointInConfig) {
|
||||||
t.Error("The error returned should have been of type ErrNoEndpointInConfig")
|
t.Error("The error returned should have been of type ErrNoEndpointInConfig")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1657,6 +1876,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
|
|||||||
Email: &email.AlertProvider{},
|
Email: &email.AlertProvider{},
|
||||||
GitHub: &github.AlertProvider{},
|
GitHub: &github.AlertProvider{},
|
||||||
GoogleChat: &googlechat.AlertProvider{},
|
GoogleChat: &googlechat.AlertProvider{},
|
||||||
|
Gotify: &gotify.AlertProvider{},
|
||||||
JetBrainsSpace: &jetbrainsspace.AlertProvider{},
|
JetBrainsSpace: &jetbrainsspace.AlertProvider{},
|
||||||
Matrix: &matrix.AlertProvider{},
|
Matrix: &matrix.AlertProvider{},
|
||||||
Mattermost: &mattermost.AlertProvider{},
|
Mattermost: &mattermost.AlertProvider{},
|
||||||
@ -1679,6 +1899,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
|
|||||||
{alertType: alert.TypeEmail, expected: alertingConfig.Email},
|
{alertType: alert.TypeEmail, expected: alertingConfig.Email},
|
||||||
{alertType: alert.TypeGitHub, expected: alertingConfig.GitHub},
|
{alertType: alert.TypeGitHub, expected: alertingConfig.GitHub},
|
||||||
{alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat},
|
{alertType: alert.TypeGoogleChat, expected: alertingConfig.GoogleChat},
|
||||||
|
{alertType: alert.TypeGotify, expected: alertingConfig.Gotify},
|
||||||
{alertType: alert.TypeJetBrainsSpace, expected: alertingConfig.JetBrainsSpace},
|
{alertType: alert.TypeJetBrainsSpace, expected: alertingConfig.JetBrainsSpace},
|
||||||
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
|
{alertType: alert.TypeMatrix, expected: alertingConfig.Matrix},
|
||||||
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
|
{alertType: alert.TypeMattermost, expected: alertingConfig.Mattermost},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
@ -1,6 +1,8 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
// ConditionResult result of a Condition
|
// ConditionResult result of a Condition
|
||||||
type ConditionResult struct {
|
type ConditionResult struct {
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
38
config/endpoint/dns/dns.go
Normal file
38
config/endpoint/dns/dns.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDNSWithNoQueryName is the error with which gatus will panic if a dns is configured without query name
|
||||||
|
ErrDNSWithNoQueryName = errors.New("you must specify a query name in the DNS configuration")
|
||||||
|
|
||||||
|
// ErrDNSWithInvalidQueryType is the error with which gatus will panic if a dns is configured with invalid query type
|
||||||
|
ErrDNSWithInvalidQueryType = errors.New("invalid query type in the DNS configuration")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config for an Endpoint of type DNS
|
||||||
|
type Config struct {
|
||||||
|
// QueryType is the type for the DNS records like A, AAAA, CNAME...
|
||||||
|
QueryType string `yaml:"query-type"`
|
||||||
|
|
||||||
|
// QueryName is the query for DNS
|
||||||
|
QueryName string `yaml:"query-name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Config) ValidateAndSetDefault() error {
|
||||||
|
if len(d.QueryName) == 0 {
|
||||||
|
return ErrDNSWithNoQueryName
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(d.QueryName, ".") {
|
||||||
|
d.QueryName += "."
|
||||||
|
}
|
||||||
|
if _, ok := dns.StringToType[d.QueryType]; !ok {
|
||||||
|
return ErrDNSWithInvalidQueryType
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
27
config/endpoint/dns/dns_test.go
Normal file
27
config/endpoint/dns/dns_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_ValidateAndSetDefault(t *testing.T) {
|
||||||
|
dns := &Config{
|
||||||
|
QueryType: "A",
|
||||||
|
QueryName: "",
|
||||||
|
}
|
||||||
|
err := dns.ValidateAndSetDefault()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error because endpoint's dns didn't have a query name, which is a mandatory field for dns")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ValidateAndSetDefaultsWithInvalidDNSQueryType(t *testing.T) {
|
||||||
|
dns := &Config{
|
||||||
|
QueryType: "B",
|
||||||
|
QueryName: "example.com",
|
||||||
|
}
|
||||||
|
err := dns.ValidateAndSetDefault()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should've returned an error because endpoint's dns query type is invalid, it needs to be a valid query name like A, AAAA, CNAME...")
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -16,12 +16,13 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
sshconfig "github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EndpointType string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HostHeader is the name of the header used to specify the host
|
// HostHeader is the name of the header used to specify the host
|
||||||
@ -36,17 +37,17 @@ const (
|
|||||||
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
// GatusUserAgent is the default user agent that Gatus uses to send requests.
|
||||||
GatusUserAgent = "Gatus/1.0"
|
GatusUserAgent = "Gatus/1.0"
|
||||||
|
|
||||||
EndpointTypeDNS EndpointType = "DNS"
|
TypeDNS Type = "DNS"
|
||||||
EndpointTypeTCP EndpointType = "TCP"
|
TypeTCP Type = "TCP"
|
||||||
EndpointTypeSCTP EndpointType = "SCTP"
|
TypeSCTP Type = "SCTP"
|
||||||
EndpointTypeUDP EndpointType = "UDP"
|
TypeUDP Type = "UDP"
|
||||||
EndpointTypeICMP EndpointType = "ICMP"
|
TypeICMP Type = "ICMP"
|
||||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
TypeSTARTTLS Type = "STARTTLS"
|
||||||
EndpointTypeTLS EndpointType = "TLS"
|
TypeTLS Type = "TLS"
|
||||||
EndpointTypeHTTP EndpointType = "HTTP"
|
TypeHTTP Type = "HTTP"
|
||||||
EndpointTypeWS EndpointType = "WEBSOCKET"
|
TypeWS Type = "WEBSOCKET"
|
||||||
EndpointTypeSSH EndpointType = "SSH"
|
TypeSSH Type = "SSH"
|
||||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
TypeUNKNOWN Type = "UNKNOWN"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -83,12 +84,6 @@ type Endpoint struct {
|
|||||||
// URL to send the request to
|
// URL to send the request to
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
|
|
||||||
// DNS is the configuration of DNS monitoring
|
|
||||||
DNS *DNS `yaml:"dns,omitempty"`
|
|
||||||
|
|
||||||
// SSH is the configuration of SSH monitoring.
|
|
||||||
SSH *SSH `yaml:"ssh,omitempty"`
|
|
||||||
|
|
||||||
// Method of the request made to the url of the endpoint
|
// Method of the request made to the url of the endpoint
|
||||||
Method string `yaml:"method,omitempty"`
|
Method string `yaml:"method,omitempty"`
|
||||||
|
|
||||||
@ -110,6 +105,12 @@ type Endpoint struct {
|
|||||||
// Alerts is the alerting configuration for the endpoint in case of failure
|
// Alerts is the alerting configuration for the endpoint in case of failure
|
||||||
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
|
||||||
|
|
||||||
|
// DNSConfig is the configuration for DNS monitoring
|
||||||
|
DNSConfig *dns.Config `yaml:"dns,omitempty"`
|
||||||
|
|
||||||
|
// SSH is the configuration for SSH monitoring
|
||||||
|
SSHConfig *sshconfig.Config `yaml:"ssh,omitempty"`
|
||||||
|
|
||||||
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
|
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
|
||||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||||
|
|
||||||
@ -127,103 +128,103 @@ type Endpoint struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled returns whether the endpoint is enabled or not
|
// IsEnabled returns whether the endpoint is enabled or not
|
||||||
func (endpoint *Endpoint) IsEnabled() bool {
|
func (e *Endpoint) IsEnabled() bool {
|
||||||
if endpoint.Enabled == nil {
|
if e.Enabled == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *endpoint.Enabled
|
return *e.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns the endpoint type
|
// Type returns the endpoint type
|
||||||
func (endpoint *Endpoint) Type() EndpointType {
|
func (e *Endpoint) Type() Type {
|
||||||
switch {
|
switch {
|
||||||
case endpoint.DNS != nil:
|
case e.DNSConfig != nil:
|
||||||
return EndpointTypeDNS
|
return TypeDNS
|
||||||
case strings.HasPrefix(endpoint.URL, "tcp://"):
|
case strings.HasPrefix(e.URL, "tcp://"):
|
||||||
return EndpointTypeTCP
|
return TypeTCP
|
||||||
case strings.HasPrefix(endpoint.URL, "sctp://"):
|
case strings.HasPrefix(e.URL, "sctp://"):
|
||||||
return EndpointTypeSCTP
|
return TypeSCTP
|
||||||
case strings.HasPrefix(endpoint.URL, "udp://"):
|
case strings.HasPrefix(e.URL, "udp://"):
|
||||||
return EndpointTypeUDP
|
return TypeUDP
|
||||||
case strings.HasPrefix(endpoint.URL, "icmp://"):
|
case strings.HasPrefix(e.URL, "icmp://"):
|
||||||
return EndpointTypeICMP
|
return TypeICMP
|
||||||
case strings.HasPrefix(endpoint.URL, "starttls://"):
|
case strings.HasPrefix(e.URL, "starttls://"):
|
||||||
return EndpointTypeSTARTTLS
|
return TypeSTARTTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "tls://"):
|
case strings.HasPrefix(e.URL, "tls://"):
|
||||||
return EndpointTypeTLS
|
return TypeTLS
|
||||||
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
case strings.HasPrefix(e.URL, "http://") || strings.HasPrefix(e.URL, "https://"):
|
||||||
return EndpointTypeHTTP
|
return TypeHTTP
|
||||||
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
case strings.HasPrefix(e.URL, "ws://") || strings.HasPrefix(e.URL, "wss://"):
|
||||||
return EndpointTypeWS
|
return TypeWS
|
||||||
case strings.HasPrefix(endpoint.URL, "ssh://"):
|
case strings.HasPrefix(e.URL, "ssh://"):
|
||||||
return EndpointTypeSSH
|
return TypeSSH
|
||||||
default:
|
default:
|
||||||
return EndpointTypeUNKNOWN
|
return TypeUNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
// ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of args that have one
|
||||||
func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
func (e *Endpoint) ValidateAndSetDefaults() error {
|
||||||
if err := validateEndpointNameGroupAndAlerts(endpoint.Name, endpoint.Group, endpoint.Alerts); err != nil {
|
if err := validateEndpointNameGroupAndAlerts(e.Name, e.Group, e.Alerts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(endpoint.URL) == 0 {
|
if len(e.URL) == 0 {
|
||||||
return ErrEndpointWithNoURL
|
return ErrEndpointWithNoURL
|
||||||
}
|
}
|
||||||
if endpoint.ClientConfig == nil {
|
if e.ClientConfig == nil {
|
||||||
endpoint.ClientConfig = client.GetDefaultConfig()
|
e.ClientConfig = client.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
if err := endpoint.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
if err := e.ClientConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig == nil {
|
if e.UIConfig == nil {
|
||||||
endpoint.UIConfig = ui.GetDefaultConfig()
|
e.UIConfig = ui.GetDefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
if err := endpoint.UIConfig.ValidateAndSetDefaults(); err != nil {
|
if err := e.UIConfig.ValidateAndSetDefaults(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.Interval == 0 {
|
if e.Interval == 0 {
|
||||||
endpoint.Interval = 1 * time.Minute
|
e.Interval = 1 * time.Minute
|
||||||
}
|
}
|
||||||
if len(endpoint.Method) == 0 {
|
if len(e.Method) == 0 {
|
||||||
endpoint.Method = http.MethodGet
|
e.Method = http.MethodGet
|
||||||
}
|
}
|
||||||
if len(endpoint.Headers) == 0 {
|
if len(e.Headers) == 0 {
|
||||||
endpoint.Headers = make(map[string]string)
|
e.Headers = make(map[string]string)
|
||||||
}
|
}
|
||||||
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
||||||
if _, userAgentHeaderExists := endpoint.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
if _, userAgentHeaderExists := e.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
||||||
endpoint.Headers[UserAgentHeader] = GatusUserAgent
|
e.Headers[UserAgentHeader] = GatusUserAgent
|
||||||
}
|
}
|
||||||
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
||||||
// and endpoint.GraphQL is set to true
|
// and endpoint.GraphQL is set to true
|
||||||
if _, contentTypeHeaderExists := endpoint.Headers[ContentTypeHeader]; !contentTypeHeaderExists && endpoint.GraphQL {
|
if _, contentTypeHeaderExists := e.Headers[ContentTypeHeader]; !contentTypeHeaderExists && e.GraphQL {
|
||||||
endpoint.Headers[ContentTypeHeader] = "application/json"
|
e.Headers[ContentTypeHeader] = "application/json"
|
||||||
}
|
}
|
||||||
if len(endpoint.Conditions) == 0 {
|
if len(e.Conditions) == 0 {
|
||||||
return ErrEndpointWithNoCondition
|
return ErrEndpointWithNoCondition
|
||||||
}
|
}
|
||||||
for _, c := range endpoint.Conditions {
|
for _, c := range e.Conditions {
|
||||||
if endpoint.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
if e.Interval < 5*time.Minute && c.hasDomainExpirationPlaceholder() {
|
||||||
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
return ErrInvalidEndpointIntervalForDomainExpirationPlaceholder
|
||||||
}
|
}
|
||||||
if err := c.Validate(); err != nil {
|
if err := c.Validate(); err != nil {
|
||||||
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
return fmt.Errorf("%v: %w", ErrInvalidConditionFormat, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.DNS != nil {
|
if e.DNSConfig != nil {
|
||||||
return endpoint.DNS.validateAndSetDefault()
|
return e.DNSConfig.ValidateAndSetDefault()
|
||||||
}
|
}
|
||||||
if endpoint.SSH != nil {
|
if e.SSHConfig != nil {
|
||||||
return endpoint.SSH.validate()
|
return e.SSHConfig.Validate()
|
||||||
}
|
}
|
||||||
if endpoint.Type() == EndpointTypeUNKNOWN {
|
if e.Type() == TypeUNKNOWN {
|
||||||
return ErrUnknownEndpointType
|
return ErrUnknownEndpointType
|
||||||
}
|
}
|
||||||
// Make sure that the request can be created
|
// Make sure that the request can be created
|
||||||
_, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body)))
|
_, err := http.NewRequest(e.Method, e.URL, bytes.NewBuffer([]byte(e.Body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -231,35 +232,35 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
|
||||||
func (endpoint *Endpoint) DisplayName() string {
|
func (e *Endpoint) DisplayName() string {
|
||||||
if len(endpoint.Group) > 0 {
|
if len(e.Group) > 0 {
|
||||||
return endpoint.Group + "/" + endpoint.Name
|
return e.Group + "/" + e.Name
|
||||||
}
|
}
|
||||||
return endpoint.Name
|
return e.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key returns the unique key for the Endpoint
|
// Key returns the unique key for the Endpoint
|
||||||
func (endpoint *Endpoint) Key() string {
|
func (e *Endpoint) Key() string {
|
||||||
return util.ConvertGroupAndEndpointNameToKey(endpoint.Group, endpoint.Name)
|
return ConvertGroupAndEndpointNameToKey(e.Group, e.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
||||||
// on configuration reload.
|
// on configuration reload.
|
||||||
// More context on https://github.com/TwiN/gatus/issues/536
|
// More context on https://github.com/TwiN/gatus/issues/536
|
||||||
func (endpoint *Endpoint) Close() {
|
func (e *Endpoint) Close() {
|
||||||
if endpoint.Type() == EndpointTypeHTTP {
|
if e.Type() == TypeHTTP {
|
||||||
client.GetHTTPClient(endpoint.ClientConfig).CloseIdleConnections()
|
client.GetHTTPClient(e.ClientConfig).CloseIdleConnections()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
// EvaluateHealth sends a request to the endpoint's URL and evaluates the conditions of the endpoint.
|
||||||
func (endpoint *Endpoint) EvaluateHealth() *Result {
|
func (e *Endpoint) EvaluateHealth() *Result {
|
||||||
result := &Result{Success: true, Errors: []string{}}
|
result := &Result{Success: true, Errors: []string{}}
|
||||||
// Parse or extract hostname from URL
|
// Parse or extract hostname from URL
|
||||||
if endpoint.DNS != nil {
|
if e.DNSConfig != nil {
|
||||||
result.Hostname = strings.TrimSuffix(endpoint.URL, ":53")
|
result.Hostname = strings.TrimSuffix(e.URL, ":53")
|
||||||
} else {
|
} else {
|
||||||
urlObject, err := url.Parse(endpoint.URL)
|
urlObject, err := url.Parse(e.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
} else {
|
} else {
|
||||||
@ -267,11 +268,11 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Retrieve IP if necessary
|
// Retrieve IP if necessary
|
||||||
if endpoint.needsToRetrieveIP() {
|
if e.needsToRetrieveIP() {
|
||||||
endpoint.getIP(result)
|
e.getIP(result)
|
||||||
}
|
}
|
||||||
// Retrieve domain expiration if necessary
|
// Retrieve domain expiration if necessary
|
||||||
if endpoint.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
if e.needsToRetrieveDomainExpiration() && len(result.Hostname) > 0 {
|
||||||
var err error
|
var err error
|
||||||
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
if result.DomainExpiration, err = client.GetDomainExpiration(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@ -279,31 +280,31 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
|||||||
}
|
}
|
||||||
// Call the endpoint (if there's no errors)
|
// Call the endpoint (if there's no errors)
|
||||||
if len(result.Errors) == 0 {
|
if len(result.Errors) == 0 {
|
||||||
endpoint.call(result)
|
e.call(result)
|
||||||
} else {
|
} else {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
// Evaluate the conditions
|
// Evaluate the conditions
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
success := condition.evaluate(result, endpoint.UIConfig.DontResolveFailedConditions)
|
success := condition.evaluate(result, e.UIConfig.DontResolveFailedConditions)
|
||||||
if !success {
|
if !success {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.Timestamp = time.Now()
|
result.Timestamp = time.Now()
|
||||||
// Clean up parameters that we don't need to keep in the results
|
// Clean up parameters that we don't need to keep in the results
|
||||||
if endpoint.UIConfig.HideURL {
|
if e.UIConfig.HideURL {
|
||||||
for errIdx, errorString := range result.Errors {
|
for errIdx, errorString := range result.Errors {
|
||||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, endpoint.URL, "<redacted>")
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, e.URL, "<redacted>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig.HideHostname {
|
if e.UIConfig.HideHostname {
|
||||||
for errIdx, errorString := range result.Errors {
|
for errIdx, errorString := range result.Errors {
|
||||||
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
result.Errors[errIdx] = strings.ReplaceAll(errorString, result.Hostname, "<redacted>")
|
||||||
}
|
}
|
||||||
result.Hostname = ""
|
result.Hostname = ""
|
||||||
}
|
}
|
||||||
if endpoint.UIConfig.HideConditions {
|
if e.UIConfig.HideConditions {
|
||||||
result.ConditionResults = nil
|
result.ConditionResults = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +329,7 @@ func (endpoint *Endpoint) EvaluateHealth() *Result {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) getIP(result *Result) {
|
func (e *Endpoint) getIP(result *Result) {
|
||||||
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
if ips, err := net.LookupIP(result.Hostname); err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
@ -337,24 +338,28 @@ func (endpoint *Endpoint) getIP(result *Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) call(result *Result) {
|
func (e *Endpoint) call(result *Result) {
|
||||||
var request *http.Request
|
var request *http.Request
|
||||||
var response *http.Response
|
var response *http.Response
|
||||||
var err error
|
var err error
|
||||||
var certificate *x509.Certificate
|
var certificate *x509.Certificate
|
||||||
endpointType := endpoint.Type()
|
endpointType := e.Type()
|
||||||
if endpointType == EndpointTypeHTTP {
|
if endpointType == TypeHTTP {
|
||||||
request = endpoint.buildHTTPRequest()
|
request = e.buildHTTPRequest()
|
||||||
}
|
}
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
if endpointType == EndpointTypeDNS {
|
if endpointType == TypeDNS {
|
||||||
endpoint.DNS.query(endpoint.URL, result)
|
result.Connected, result.DNSRCode, result.Body, err = client.QueryDNS(e.DNSConfig.QueryType, e.DNSConfig.QueryName, e.URL)
|
||||||
|
if err != nil {
|
||||||
|
result.AddError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSTARTTLS || endpointType == EndpointTypeTLS {
|
} else if endpointType == TypeSTARTTLS || endpointType == TypeTLS {
|
||||||
if endpointType == EndpointTypeSTARTTLS {
|
if endpointType == TypeSTARTTLS {
|
||||||
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(endpoint.URL, "starttls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformStartTLS(strings.TrimPrefix(e.URL, "starttls://"), e.ClientConfig)
|
||||||
} else {
|
} else {
|
||||||
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(endpoint.URL, "tls://"), endpoint.ClientConfig)
|
result.Connected, certificate, err = client.CanPerformTLS(strings.TrimPrefix(e.URL, "tls://"), e.ClientConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@ -362,39 +367,39 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
result.CertificateExpiration = time.Until(certificate.NotAfter)
|
||||||
} else if endpointType == EndpointTypeTCP {
|
} else if endpointType == TypeTCP {
|
||||||
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(endpoint.URL, "tcp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateTCPConnection(strings.TrimPrefix(e.URL, "tcp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeUDP {
|
} else if endpointType == TypeUDP {
|
||||||
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(endpoint.URL, "udp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateUDPConnection(strings.TrimPrefix(e.URL, "udp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSCTP {
|
} else if endpointType == TypeSCTP {
|
||||||
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(endpoint.URL, "sctp://"), endpoint.ClientConfig)
|
result.Connected = client.CanCreateSCTPConnection(strings.TrimPrefix(e.URL, "sctp://"), e.ClientConfig)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeICMP {
|
} else if endpointType == TypeICMP {
|
||||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(e.URL, "icmp://"), e.ClientConfig)
|
||||||
} else if endpointType == EndpointTypeWS {
|
} else if endpointType == TypeWS {
|
||||||
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig)
|
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.Body, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else if endpointType == EndpointTypeSSH {
|
} else if endpointType == TypeSSH {
|
||||||
var cli *ssh.Client
|
var cli *ssh.Client
|
||||||
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
|
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(e.URL, "ssh://"), e.SSHConfig.Username, e.SSHConfig.Password, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
|
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.Body, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
} else {
|
} else {
|
||||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
response, err = client.GetHTTPClient(e.ClientConfig).Do(request)
|
||||||
result.Duration = time.Since(startTime)
|
result.Duration = time.Since(startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError(err.Error())
|
result.AddError(err.Error())
|
||||||
@ -408,7 +413,7 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
result.HTTPStatus = response.StatusCode
|
result.HTTPStatus = response.StatusCode
|
||||||
result.Connected = response.StatusCode > 0
|
result.Connected = response.StatusCode > 0
|
||||||
// Only read the Body if there's a condition that uses the BodyPlaceholder
|
// Only read the Body if there's a condition that uses the BodyPlaceholder
|
||||||
if endpoint.needsToReadBody() {
|
if e.needsToReadBody() {
|
||||||
result.Body, err = io.ReadAll(response.Body)
|
result.Body, err = io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.AddError("error reading response body:" + err.Error())
|
result.AddError("error reading response body:" + err.Error())
|
||||||
@ -417,19 +422,19 @@ func (endpoint *Endpoint) call(result *Result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
func (e *Endpoint) buildHTTPRequest() *http.Request {
|
||||||
var bodyBuffer *bytes.Buffer
|
var bodyBuffer *bytes.Buffer
|
||||||
if endpoint.GraphQL {
|
if e.GraphQL {
|
||||||
graphQlBody := map[string]string{
|
graphQlBody := map[string]string{
|
||||||
"query": endpoint.Body,
|
"query": e.Body,
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(graphQlBody)
|
body, _ := json.Marshal(graphQlBody)
|
||||||
bodyBuffer = bytes.NewBuffer(body)
|
bodyBuffer = bytes.NewBuffer(body)
|
||||||
} else {
|
} else {
|
||||||
bodyBuffer = bytes.NewBuffer([]byte(endpoint.Body))
|
bodyBuffer = bytes.NewBuffer([]byte(e.Body))
|
||||||
}
|
}
|
||||||
request, _ := http.NewRequest(endpoint.Method, endpoint.URL, bodyBuffer)
|
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
||||||
for k, v := range endpoint.Headers {
|
for k, v := range e.Headers {
|
||||||
request.Header.Set(k, v)
|
request.Header.Set(k, v)
|
||||||
if k == HostHeader {
|
if k == HostHeader {
|
||||||
request.Host = v
|
request.Host = v
|
||||||
@ -439,11 +444,11 @@ func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
// needsToReadBody checks if there's any condition that requires the response Body to be read
|
||||||
func (endpoint *Endpoint) needsToReadBody() bool {
|
func (e *Endpoint) needsToReadBody() bool {
|
||||||
if strings.Contains(endpoint.OnErrorAdd, BodyPlaceholder) {
|
if strings.Contains(e.OnErrorAdd, BodyPlaceholder) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasBodyPlaceholder() {
|
if condition.hasBodyPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -452,8 +457,8 @@ func (endpoint *Endpoint) needsToReadBody() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
// needsToRetrieveDomainExpiration checks if there's any condition that requires a whois query to be performed
|
||||||
func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
func (e *Endpoint) needsToRetrieveDomainExpiration() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasDomainExpirationPlaceholder() {
|
if condition.hasDomainExpirationPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -462,8 +467,8 @@ func (endpoint *Endpoint) needsToRetrieveDomainExpiration() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
// needsToRetrieveIP checks if there's any condition that requires an IP lookup
|
||||||
func (endpoint *Endpoint) needsToRetrieveIP() bool {
|
func (e *Endpoint) needsToRetrieveIP() bool {
|
||||||
for _, condition := range endpoint.Conditions {
|
for _, condition := range e.Conditions {
|
||||||
if condition.hasIPPlaceholder() {
|
if condition.hasIPPlaceholder() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -13,7 +13,9 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/client"
|
"github.com/TwiN/gatus/v5/client"
|
||||||
"github.com/TwiN/gatus/v5/core/ui"
|
"github.com/TwiN/gatus/v5/config/endpoint/dns"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ssh"
|
||||||
|
"github.com/TwiN/gatus/v5/config/endpoint/ui"
|
||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -302,105 +304,105 @@ func TestEndpoint_IsEnabled(t *testing.T) {
|
|||||||
func TestEndpoint_Type(t *testing.T) {
|
func TestEndpoint_Type(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
URL string
|
URL string
|
||||||
DNS *DNS
|
DNS *dns.Config
|
||||||
SSH *SSH
|
SSH *ssh.Config
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
args args
|
args args
|
||||||
want EndpointType
|
want Type
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "8.8.8.8",
|
URL: "8.8.8.8",
|
||||||
DNS: &DNS{
|
DNS: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: EndpointTypeDNS,
|
want: TypeDNS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "tcp://127.0.0.1:6379",
|
URL: "tcp://127.0.0.1:6379",
|
||||||
},
|
},
|
||||||
want: EndpointTypeTCP,
|
want: TypeTCP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "icmp://example.com",
|
URL: "icmp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeICMP,
|
want: TypeICMP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "sctp://example.com",
|
URL: "sctp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeSCTP,
|
want: TypeSCTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "udp://example.com",
|
URL: "udp://example.com",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUDP,
|
want: TypeUDP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "starttls://smtp.gmail.com:587",
|
URL: "starttls://smtp.gmail.com:587",
|
||||||
},
|
},
|
||||||
want: EndpointTypeSTARTTLS,
|
want: TypeSTARTTLS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "tls://example.com:443",
|
URL: "tls://example.com:443",
|
||||||
},
|
},
|
||||||
want: EndpointTypeTLS,
|
want: TypeTLS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "https://twin.sh/health",
|
URL: "https://twin.sh/health",
|
||||||
},
|
},
|
||||||
want: EndpointTypeHTTP,
|
want: TypeHTTP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "wss://example.com/",
|
URL: "wss://example.com/",
|
||||||
},
|
},
|
||||||
want: EndpointTypeWS,
|
want: TypeWS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "ws://example.com/",
|
URL: "ws://example.com/",
|
||||||
},
|
},
|
||||||
want: EndpointTypeWS,
|
want: TypeWS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "ssh://example.com:22",
|
URL: "ssh://example.com:22",
|
||||||
SSH: &SSH{
|
SSH: &ssh.Config{
|
||||||
Username: "root",
|
Username: "root",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: EndpointTypeSSH,
|
want: TypeSSH,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "invalid://example.org",
|
URL: "invalid://example.org",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUNKNOWN,
|
want: TypeUNKNOWN,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: args{
|
args: args{
|
||||||
URL: "no-scheme",
|
URL: "no-scheme",
|
||||||
},
|
},
|
||||||
want: EndpointTypeUNKNOWN,
|
want: TypeUNKNOWN,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(string(tt.want), func(t *testing.T) {
|
t.Run(string(tt.want), func(t *testing.T) {
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
URL: tt.args.URL,
|
URL: tt.args.URL,
|
||||||
DNS: tt.args.DNS,
|
DNSConfig: tt.args.DNS,
|
||||||
}
|
}
|
||||||
if got := endpoint.Type(); got != tt.want {
|
if got := endpoint.Type(); got != tt.want {
|
||||||
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
t.Errorf("Endpoint.Type() = %v, want %v", got, tt.want)
|
||||||
@ -500,7 +502,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
|||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "dns-test",
|
Name: "dns-test",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
DNS: &DNS{
|
DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com",
|
QueryName: "example.com",
|
||||||
},
|
},
|
||||||
@ -510,7 +512,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("did not expect an error, got", err)
|
t.Error("did not expect an error, got", err)
|
||||||
}
|
}
|
||||||
if endpoint.DNS.QueryName != "example.com." {
|
if endpoint.DNSConfig.QueryName != "example.com." {
|
||||||
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
t.Error("Endpoint.dns.query-name should be formatted with . suffix")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -526,13 +528,13 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
|||||||
name: "fail when has no user",
|
name: "fail when has no user",
|
||||||
username: "",
|
username: "",
|
||||||
password: "password",
|
password: "password",
|
||||||
expectedErr: ErrEndpointWithoutSSHUsername,
|
expectedErr: ssh.ErrEndpointWithoutSSHUsername,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail when has no password",
|
name: "fail when has no password",
|
||||||
username: "username",
|
username: "username",
|
||||||
password: "",
|
password: "",
|
||||||
expectedErr: ErrEndpointWithoutSSHPassword,
|
expectedErr: ssh.ErrEndpointWithoutSSHPassword,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "success when all fields are set",
|
name: "success when all fields are set",
|
||||||
@ -547,7 +549,7 @@ func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
|||||||
endpoint := &Endpoint{
|
endpoint := &Endpoint{
|
||||||
Name: "ssh-test",
|
Name: "ssh-test",
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: scenario.username,
|
Username: scenario.username,
|
||||||
Password: scenario.password,
|
Password: scenario.password,
|
||||||
},
|
},
|
||||||
@ -786,7 +788,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
|||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
URL: "8.8.8.8",
|
URL: "8.8.8.8",
|
||||||
DNS: &DNS{
|
DNSConfig: &dns.Config{
|
||||||
QueryType: "A",
|
QueryType: "A",
|
||||||
QueryName: "example.com.",
|
QueryName: "example.com.",
|
||||||
},
|
},
|
||||||
@ -809,7 +811,7 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||||
tests := []struct {
|
scenarios := []struct {
|
||||||
name string
|
name string
|
||||||
endpoint Endpoint
|
endpoint Endpoint
|
||||||
conditions []Condition
|
conditions []Condition
|
||||||
@ -820,9 +822,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
endpoint: Endpoint{
|
endpoint: Endpoint{
|
||||||
Name: "ssh-success",
|
Name: "ssh-success",
|
||||||
URL: "ssh://localhost",
|
URL: "ssh://localhost",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: "test",
|
Username: "scenario",
|
||||||
Password: "test",
|
Password: "scenario",
|
||||||
},
|
},
|
||||||
Body: "{ \"command\": \"uptime\" }",
|
Body: "{ \"command\": \"uptime\" }",
|
||||||
},
|
},
|
||||||
@ -834,9 +836,9 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
endpoint: Endpoint{
|
endpoint: Endpoint{
|
||||||
Name: "ssh-failure",
|
Name: "ssh-failure",
|
||||||
URL: "ssh://localhost",
|
URL: "ssh://localhost",
|
||||||
SSH: &SSH{
|
SSHConfig: &ssh.Config{
|
||||||
Username: "test",
|
Username: "scenario",
|
||||||
Password: "test",
|
Password: "scenario",
|
||||||
},
|
},
|
||||||
Body: "{ \"command\": \"uptime\" }",
|
Body: "{ \"command\": \"uptime\" }",
|
||||||
},
|
},
|
||||||
@ -845,13 +847,13 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, scenario := range scenarios {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
test.endpoint.ValidateAndSetDefaults()
|
scenario.endpoint.ValidateAndSetDefaults()
|
||||||
test.endpoint.Conditions = test.conditions
|
scenario.endpoint.Conditions = scenario.conditions
|
||||||
result := test.endpoint.EvaluateHealth()
|
result := scenario.endpoint.EvaluateHealth()
|
||||||
if result.Success != test.success {
|
if result.Success != scenario.success {
|
||||||
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
|
t.Errorf("Expected success to be %v, but was %v", scenario.success, result.Success)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Event is something that happens at a specific time
|
// Event is something that happens at a specific time
|
||||||
type Event struct {
|
type Event struct {
|
@ -1,6 +1,8 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestNewEventFromResult(t *testing.T) {
|
func TestNewEventFromResult(t *testing.T) {
|
||||||
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
if event := NewEventFromResult(&Result{Success: true}); event.Type != EventHealthy {
|
@ -1,10 +1,9 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -46,11 +45,6 @@ func (externalEndpoint *ExternalEndpoint) ValidateAndSetDefaults() error {
|
|||||||
if len(externalEndpoint.Token) == 0 {
|
if len(externalEndpoint.Token) == 0 {
|
||||||
return ErrExternalEndpointWithNoToken
|
return ErrExternalEndpointWithNoToken
|
||||||
}
|
}
|
||||||
for _, externalEndpointAlert := range externalEndpoint.Alerts {
|
|
||||||
if err := externalEndpointAlert.ValidateAndSetDefaults(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +66,7 @@ func (externalEndpoint *ExternalEndpoint) DisplayName() string {
|
|||||||
|
|
||||||
// Key returns the unique key for the Endpoint
|
// Key returns the unique key for the Endpoint
|
||||||
func (externalEndpoint *ExternalEndpoint) Key() string {
|
func (externalEndpoint *ExternalEndpoint) Key() string {
|
||||||
return util.ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name)
|
return ConvertGroupAndEndpointNameToKey(externalEndpoint.Group, externalEndpoint.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToEndpoint converts the ExternalEndpoint to an Endpoint
|
// ToEndpoint converts the ExternalEndpoint to an Endpoint
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
@ -1,4 +1,4 @@
|
|||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
@ -1,4 +1,4 @@
|
|||||||
package util
|
package endpoint
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@ -29,7 +29,7 @@ type Result struct {
|
|||||||
// Errors encountered during the evaluation of the Endpoint's health
|
// Errors encountered during the evaluation of the Endpoint's health
|
||||||
Errors []string `json:"errors,omitempty"`
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
|
||||||
// ConditionResults results of the Endpoint's conditions
|
// ConditionResults are the results of each of the Endpoint's Condition
|
||||||
ConditionResults []*ConditionResult `json:"conditionResults,omitempty"`
|
ConditionResults []*ConditionResult `json:"conditionResults,omitempty"`
|
||||||
|
|
||||||
// Success whether the result signifies a success or not
|
// Success whether the result signifies a success or not
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user