Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: add headers support #178

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ Here are some examples of conditions you can use:
| `[BODY].name == pat(john*)` | String at JSONPath `$.name` matches pattern `john*` | `{"name":"john.doe"}` | `{"name":"bob"}` |
| `[BODY].id == any(1, 2)` | Value at JSONPath `$.id` is equal to `1` or `2` | 1, 2 | 3, 4, 5 |
| `[CERTIFICATE_EXPIRATION] > 48h` | Certificate expiration is more than 48h away | 49h, 50h, 123h | 1h, 24h, ... |
| `[HEADER].Content-Type == text/html` | HTTP response header `Content-Type` must be `text/html` | `text/html` | `application/json`, ... |
| `[HEADER].Server == pat(nginx/1.*)` | HTTP response header `Server` matches parrern `nginx/1.*` | `nginx/1.21.1`, `nginx/1.19.7`, ... | `nginx/0.9.1`, ... |


#### Placeholders
Expand All @@ -219,6 +221,7 @@ Here are some examples of conditions you can use:
| `[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)
| `[DNS_RCODE]` | Resolves into the DNS status of the response | NOERROR
| `[HEADER].<header_name>` | Resolves into the HTTP response specified header | `text/html`


#### Functions
Expand Down
76 changes: 48 additions & 28 deletions core/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"time"
"net/http"

"github.com/TwinProduction/gatus/jsonpath"
"github.com/TwinProduction/gatus/pattern"
Expand Down Expand Up @@ -46,6 +47,11 @@ const (
// Values that could replace the placeholder: 4461677039 (~52 days)
CertificateExpirationPlaceholder = "[CERTIFICATE_EXPIRATION]"

// HeaderPlaceholder is a placeholder for a HTTP header
//
// Values that could replace the placeholder: []
HeaderPlaceholder = "[HEADER]"

// LengthFunctionPrefix is the prefix for the length function
//
// Usage: len([BODY].articles) == 10, len([BODY].name) > 5
Expand Down Expand Up @@ -220,40 +226,54 @@ func sanitizeAndResolve(elements []string, result *Result) ([]string, []string)
case CertificateExpirationPlaceholder:
element = strconv.FormatInt(result.CertificateExpiration.Milliseconds(), 10)
default:
// if contains the BodyPlaceholder, then evaluate json path
if strings.Contains(element, BodyPlaceholder) {
checkingForLength := false
checkingForExistence := false
if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
checkingForLength = true
element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix)
}
if strings.HasPrefix(element, HasFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
checkingForExistence = true
element = strings.TrimSuffix(strings.TrimPrefix(element, HasFunctionPrefix), FunctionSuffix)
// if contains the HeaderPlaceholder
if strings.HasPrefix(strings.ToUpper(element), HeaderPlaceholder) {
headerName := http.CanonicalHeaderKey(strings.Split(element, ".")[1])
headerValues, headerExists := result.Headers[headerName]
if headerExists {
headerValuesFormatted := strings.Join(headerValues, ", ")
element = "any(" + headerValuesFormatted + ")"
fmt.Println(element)
} else {
// Specified header key isn't present in response headers
element = "any()"
}
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.TrimPrefix(strings.TrimPrefix(element, BodyPlaceholder), "."), result.body)
if checkingForExistence {
if err != nil {
element = "false"
} else {
element = "true"
} else {
// if contains the BodyPlaceholder, then evaluate json path
if strings.Contains(element, BodyPlaceholder) {
checkingForLength := false
checkingForExistence := false
if strings.HasPrefix(element, LengthFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
checkingForLength = true
element = strings.TrimSuffix(strings.TrimPrefix(element, LengthFunctionPrefix), FunctionSuffix)
}
} else {
if err != nil {
if err.Error() != "unexpected end of JSON input" {
result.AddError(err.Error())
}
if checkingForLength {
element = LengthFunctionPrefix + element + FunctionSuffix + " " + InvalidConditionElementSuffix
if strings.HasPrefix(element, HasFunctionPrefix) && strings.HasSuffix(element, FunctionSuffix) {
checkingForExistence = true
element = strings.TrimSuffix(strings.TrimPrefix(element, HasFunctionPrefix), FunctionSuffix)
}
resolvedElement, resolvedElementLength, err := jsonpath.Eval(strings.TrimPrefix(strings.TrimPrefix(element, BodyPlaceholder), "."), result.body)
if checkingForExistence {
if err != nil {
element = "false"
} else {
element = element + " " + InvalidConditionElementSuffix
element = "true"
}
} else {
if checkingForLength {
element = strconv.Itoa(resolvedElementLength)
if err != nil {
if err.Error() != "unexpected end of JSON input" {
result.AddError(err.Error())
}
if checkingForLength {
element = LengthFunctionPrefix + element + FunctionSuffix + " " + InvalidConditionElementSuffix
} else {
element = element + " " + InvalidConditionElementSuffix
}
} else {
element = resolvedElement
if checkingForLength {
element = strconv.Itoa(resolvedElementLength)
} else {
element = resolvedElement
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions core/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type Result struct {
// CertificateExpiration is the duration before the certificate expires
CertificateExpiration time.Duration `json:"-"`

// Headers headers of HTTP response
Headers map[string][]string `json:"headers"`

// body is the response body
//
// Note that this variable is only used during the evaluation of a service's health.
Expand Down
1 change: 1 addition & 0 deletions core/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ func (service *Service) call(result *Result) {
}
result.HTTPStatus = response.StatusCode
result.Connected = response.StatusCode > 0
result.Headers = response.Header
// Only read the body if there's a condition that uses the BodyPlaceholder
if service.needsToReadBody() {
result.body, err = ioutil.ReadAll(response.Body)
Expand Down