Compare commits

...

18 Commits

Author SHA1 Message Date
TwinProduction
be72a73082 Fix potential panic 2020-11-17 12:35:21 -05:00
TwinProduction
5433653cbb Fix bad example 2020-11-17 12:33:00 -05:00
Chris C
60dea90921 Merge pull request #41 from Exagone313/basic-duration-string
Add basic duration comparison
2020-11-17 12:20:11 -05:00
TwinProduction
e79c849e6d Revert "Add day format support for duration comparison"
This reverts commit 21509428
2020-11-17 12:16:40 -05:00
Chris C
07ca8be732 Merge pull request #42 from anapsix/misc-improvements
Misc improvements
2020-11-17 10:32:46 -05:00
Anastas Dancha
4a84368d24 use COPY instead of ADD, install pkgs then build
- using COPY instead of ADD, since working with local paths, and
    not extracting archived data
  - installing packages before building application binary improves caching,
    and avoids installing packages every time the application code changes

Signed-off-by: Anastas Dancha <anapsix@random.io>
2020-11-17 13:15:31 +03:00
Elouan Martinet
2150942876 Add day format support for duration comparison 2020-11-16 18:16:11 +01:00
TwinProduction
c23e21cb41 Update documentation for CERTIFICATE_EXPIRATION placeholder 2020-11-16 12:03:53 -05:00
TwinProduction
5699a1c236 Improve test coverage for duration parsing 2020-11-16 10:27:46 -05:00
TwinProduction
573b5f89e1 Improve test coverage 2020-11-16 10:10:02 -05:00
Anastas Dancha
108a88ae57 improving build caching
- adding `Dockerfile` and `.git/` to `.dockeringore` improves
    caching when building images when these paths have changes
    unrelated to application functionality

Signed-off-by: Anastas Dancha <anapsix@random.io>
2020-11-16 17:18:06 +03:00
Elouan Martinet
121369d9c0 Add basic duration comparison 2020-11-16 09:32:37 +01:00
Chris C
d1f24dbea4 Merge pull request #37 from Exagone313/certificate-expiration
Add certificate expiration support
2020-11-15 13:50:49 -05:00
Elouan Martinet
27c8c552a2 Add tests for certificate expiration 2020-11-15 18:47:28 +01:00
Elouan Martinet
e0109e79af Add documentation for CERTIFICATE_EXPIRATION placeholder 2020-11-15 18:34:54 +01:00
Elouan Martinet
7d97e83875 Add support for comparing duration before certificate expiration 2020-11-15 18:33:09 +01:00
Elouan Martinet
d50721c8f0 Compare numeric values as int64 2020-11-15 16:50:05 +01:00
TwinProduction
d184786fd1 Update alerting provider order 2020-11-14 18:52:01 -05:00
7 changed files with 147 additions and 52 deletions

View File

@@ -1,3 +1,5 @@
example
Dockerfile
.github
.idea
.git

View File

@@ -1,9 +1,9 @@
# Build the go application into a binary
FROM golang:alpine as builder
WORKDIR /app
ADD . ./
RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus .
RUN apk --update add ca-certificates
WORKDIR /app
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus .
# Run Tests inside docker image if you don't have a configured go environment
#RUN apk update && apk add --virtual build-dependencies build-base gcc
@@ -17,4 +17,4 @@ COPY --from=builder /app/static static/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENV PORT=8080
EXPOSE ${PORT}
ENTRYPOINT ["/gatus"]
ENTRYPOINT ["/gatus"]

View File

@@ -24,9 +24,9 @@ core applications: https://status.twinnation.org/
- [Functions](#functions)
- [Alerting](#alerting)
- [Configuring Slack alerts](#configuring-slack-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
- [Configuring Twilio alerts](#configuring-twilio-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
- [Configuring custom alerts](#configuring-custom-alerts)
- [Kubernetes (ALPHA)](#kubernetes-alpha)
- [Auto Discovery](#auto-discovery)
@@ -103,7 +103,7 @@ Note that you can also add environment variables in the configuration file (i.e.
| `services[].graphql` | Whether to wrap the body in a query param (`{"query":"$body"}`) | `false` |
| `services[].body` | Request body | `""` |
| `services[].headers` | Request headers | `{}` |
| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `pagerduty`, `twilio`, `custom`, `mattermost` | Required `""` |
| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `pagerduty`, `twilio`, `mattermost`, `custom` | Required `""` |
| `services[].alerts[].enabled` | Whether to enable the alert | `false` |
| `services[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert | `3` |
| `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | `2` |
@@ -112,9 +112,6 @@ Note that you can also add environment variables in the configuration file (i.e.
| `alerting` | Configuration for alerting | `{}` |
| `alerting.slack` | Configuration for alerts of type `slack` | `{}` |
| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
| `alerting.mattermost.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` |
| `alerting.pagerduty` | Configuration for alerts of type `pagerduty` | `{}` |
| `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key. | Required `""` |
| `alerting.twilio` | Settings for alerts of type `twilio` | `{}` |
@@ -122,6 +119,9 @@ Note that you can also add environment variables in the configuration file (i.e.
| `alerting.twilio.token` | Twilio auth token | Required `""` |
| `alerting.twilio.from` | Number to send Twilio alerts from | Required `""` |
| `alerting.twilio.to` | Number to send twilio alerts to | Required `""` |
| `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` |
| `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` |
| `alerting.mattermost.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` |
| `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` |
| `alerting.custom.url` | Custom alerting request url | Required `""` |
| `alerting.custom.method` | Request method | `GET` |
@@ -157,17 +157,19 @@ Here are some examples of conditions you can use:
| `len([BODY].data) < 5` | Array at JSONPath `$.data` has less than 5 elements | `{"data":[{"id":1}]}` | |
| `len([BODY].name) == 8` | String at JSONPath `$.name` has a length of 8 | `{"name":"john.doe"}` | `{"name":"bob"}` |
| `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` |
| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | 49h, 50h, 123h | 1h, 24h, ... |
#### Placeholders
| Placeholder | Description | Example of resolved value |
|:----------------------- |:------------------------------------------------------- |:------------------------- |
| `[STATUS]` | Resolves into the HTTP status of the request | 404
| `[RESPONSE_TIME]` | Resolves into the response time the request took, in ms | 10
| `[IP]` | Resolves into the IP of the target host | 192.168.0.232
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}`
| `[CONNECTED]` | Resolves into whether a connection could be established | `true`
| Placeholder | Description | Example of resolved value |
|:-------------------------- |:--------------------------------------------------------------- |:------------------------- |
| `[STATUS]` | Resolves into the HTTP status of the request | 404
| `[RESPONSE_TIME]` | Resolves into the response time the request took, in ms | 10
| `[IP]` | Resolves into the IP of the target host | 192.168.0.232
| `[BODY]` | Resolves into the response body. Supports JSONPath. | `{"name":"john.doe"}`
| `[CONNECTED]` | Resolves into whether a connection could be established | `true`
| `[CERTIFICATE_EXPIRATION]` | Resolves into the duration before certificate expiration | `24h`, `48h`, 0 (if not using HTTPS)
#### Functions
@@ -214,37 +216,6 @@ Here's an example of what the notifications look like:
![Slack notifications](.github/assets/slack-alerts.png)
#### Configuring Mattermost alerts
```yaml
alerting:
mattermost:
webhook-url: "http://**********/hooks/**********"
insecure: true
services:
- name: twinnation
url: "https://twinnation.org/health"
interval: 30s
alerts:
- type: mattermost
enabled: true
description: "healthcheck failed 3 times in a row"
send-on-resolved: true
- type: mattermost
enabled: true
failure-threshold: 5
description: "healthcheck failed 5 times in a row"
send-on-resolved: true
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 300"
```
Here's an example of what the notifications look like:
![Mattermost notifications](.github/assets/mattermost-alerts.png)
#### Configuring PagerDuty alerts
It is highly recommended to set `services[].alerts[].send-on-resolved` to `true` for alerts
@@ -300,6 +271,33 @@ services:
```
#### Configuring Mattermost alerts
```yaml
alerting:
mattermost:
webhook-url: "http://**********/hooks/**********"
insecure: true
services:
- name: twinnation
url: "https://twinnation.org/health"
interval: 30s
alerts:
- type: mattermost
enabled: true
description: "healthcheck failed 3 times in a row"
send-on-resolved: true
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 300"
```
Here's an example of what the notifications look like:
![Mattermost notifications](.github/assets/mattermost-alerts.png)
#### Configuring custom alerts
While they're called alerts, you can use this feature to call anything.

View File

@@ -2,11 +2,13 @@ package core
import (
"fmt"
"github.com/TwinProduction/gatus/jsonpath"
"github.com/TwinProduction/gatus/pattern"
"log"
"strconv"
"strings"
"time"
"github.com/TwinProduction/gatus/jsonpath"
"github.com/TwinProduction/gatus/pattern"
)
const (
@@ -35,6 +37,11 @@ const (
// Values that could replace the placeholder: true, false
ConnectedPlaceHolder = "[CONNECTED]"
// CertificateExpirationPlaceholder is a placeholder for the duration before certificate expiration, in milliseconds.
//
// Values that could replace the placeholder: 4461677039 (~52 days)
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"
// LengthFunctionPrefix is the prefix for the length function
LengthFunctionPrefix = "len("
@@ -142,6 +149,8 @@ func sanitizeAndResolve(list []string, result *Result) []string {
element = body
case ConnectedPlaceHolder:
element = strconv.FormatBool(result.Connected)
case CertificateExpirationPlaceholder:
element = strconv.FormatInt(int64(result.CertificateExpiration.Milliseconds()), 10)
default:
// if contains the BodyPlaceHolder, then evaluate json path
if strings.Contains(element, BodyPlaceHolder) {
@@ -174,11 +183,13 @@ func sanitizeAndResolve(list []string, result *Result) []string {
return sanitizedList
}
func sanitizeAndResolveNumerical(list []string, result *Result) []int {
var sanitizedNumbers []int
func sanitizeAndResolveNumerical(list []string, result *Result) []int64 {
var sanitizedNumbers []int64
sanitizedList := sanitizeAndResolve(list, result)
for _, element := range sanitizedList {
if number, err := strconv.Atoi(element); err != nil {
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
sanitizedNumbers = append(sanitizedNumbers, duration.Milliseconds())
} else if number, err := strconv.ParseInt(element, 10, 64); err != nil {
// Default to 0 if the string couldn't be converted to an integer
sanitizedNumbers = append(sanitizedNumbers, 0)
} else {

View File

@@ -1,6 +1,7 @@
package core
import (
"strconv"
"testing"
"time"
)
@@ -59,7 +60,27 @@ func TestCondition_evaluateWithResponseTimeUsingLessThan(t *testing.T) {
}
}
func TestCondition_evaluateWithResponseTimeUsingLessThanDuration(t *testing.T) {
condition := Condition("[RESPONSE_TIME] < 1s")
result := &Result{Duration: time.Millisecond * 50}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingLessThanInvalid(t *testing.T) {
condition := Condition("[RESPONSE_TIME] < potato")
result := &Result{Duration: time.Millisecond * 50}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have failed because the condition has an invalid numerical value that should've automatically resolved to 0", condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
// Not exactly sure why you'd want to have a condition that checks if the response time is too fast,
// but hey, who am I to judge?
condition := Condition("[RESPONSE_TIME] > 500")
result := &Result{Duration: time.Millisecond * 750}
condition.evaluate(result)
@@ -68,6 +89,15 @@ func TestCondition_evaluateWithResponseTimeUsingGreaterThan(t *testing.T) {
}
}
func TestCondition_evaluateWithResponseTimeUsingGreaterThanDuration(t *testing.T) {
condition := Condition("[RESPONSE_TIME] > 1s")
result := &Result{Duration: time.Second * 2}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestCondition_evaluateWithResponseTimeUsingGreaterThanOrEqualTo(t *testing.T) {
condition := Condition("[RESPONSE_TIME] >= 500")
result := &Result{Duration: time.Millisecond * 500}
@@ -309,3 +339,50 @@ func TestCondition_evaluateWithConnectedFailure(t *testing.T) {
t.Errorf("Condition '%s' should have been a failure", condition)
}
}
func TestCondition_evaluateWithUnsetCertificateExpiration(t *testing.T) {
condition := Condition("[CERTIFICATE_EXPIRATION] == 0")
result := &Result{}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanNumerical(t *testing.T) {
acceptable := (time.Hour * 24 * 28).Milliseconds()
condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10))
result := &Result{CertificateExpiration: time.Hour * 24 * 60}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanNumericalFailure(t *testing.T) {
acceptable := (time.Hour * 24 * 28).Milliseconds()
condition := Condition("[CERTIFICATE_EXPIRATION] > " + strconv.FormatInt(acceptable, 10))
result := &Result{CertificateExpiration: time.Hour * 24 * 14}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanDuration(t *testing.T) {
condition := Condition("[CERTIFICATE_EXPIRATION] > 12h")
result := &Result{CertificateExpiration: 24 * time.Hour}
condition.evaluate(result)
if !result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a success", condition)
}
}
func TestCondition_evaluateWithCertificateExpirationGreaterThanDurationFailure(t *testing.T) {
condition := Condition("[CERTIFICATE_EXPIRATION] > 48h")
result := &Result{CertificateExpiration: 24 * time.Hour}
condition.evaluate(result)
if result.ConditionResults[0].Success {
t.Errorf("Condition '%s' should have been a failure", condition)
}
}

View File

@@ -169,6 +169,10 @@ func (service *Service) call(result *Result) {
result.Errors = append(result.Errors, err.Error())
return
}
if response.TLS != nil && len(response.TLS.PeerCertificates) > 0 {
certificate := response.TLS.PeerCertificates[0]
result.CertificateExpiration = certificate.NotAfter.Sub(time.Now())
}
result.HTTPStatus = response.StatusCode
result.Connected = response.StatusCode > 0
result.Body, err = ioutil.ReadAll(response.Body)

View File

@@ -45,6 +45,9 @@ type Result struct {
// Timestamp when the request was sent
Timestamp time.Time `json:"timestamp"`
// CertificateExpiration is the duration before the certificate expires
CertificateExpiration time.Duration `json:"certificate-expiration,omitempty"`
}
// ConditionResult result of a Condition