Skip to content

Commit

Permalink
Change query to use gojq (#5)
Browse files Browse the repository at this point in the history
* Transition from path using gojsonq to query using gojq
* Update CHANGELOG

Signed-off-by: Todd Campbell <todd@sensu.io>
  • Loading branch information
Todd Campbell authored Jan 20, 2021
1 parent 045d230 commit c49d0c9
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 53 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### BREAKING CHANGE
- Changed from --path using gojsonq to --query using gojq

## [0.2.0] - 2021-01-13

### Changed
Expand Down
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,42 +173,50 @@ Available Commands:
Flags:
-u, --url string URL to test (default "http://localhost:80/")
-T, --timeout int Request timeout in seconds (default 15)
-p, --path string Path to query in JSON
-e, --expression string Expression to query in JSON
-q, --query string Query written in jq format
-e, --expression string Expression for comparing result of query
-H, --header strings Additional header(s) to send in check request
-t, --trusted-ca-file string TLS CA certificate bundle in PEM format
-i, --insecure-skip-verify Skip TLS certificate verification (not recommended!)
-t, --trusted-ca-file string TLS CA certificate bundle in PEM format
-T, --timeout int Request timeout in seconds (default 15)
-h, --help help for http-json
Use "http-json [command] --help" for more information about a command.
```

#### Writing queries and expressions

Queries are written in [jq][6] format as implemented by the [gojq][7] library.
The query is used to obtain a value in the JSON requested from the URL
specified by `--url`. This value is then evaluated against the expression
provided by `--expression`.

#### Example(s)

```
# Boolean example - checking Sensu cluster health
http-json --url http://backend:8080/health --path ClusterHealth.[0].Healthy --expression "== true"
http-json OK: The value true found at ClusterHealth.[0].Healthy matched with expression "== true" and returned true
http-json --url http://backend:8080/health --query ".ClusterHealth.[0].Healthy" --expression "== true"
http-json OK: The value true found at .ClusterHealth.[0].Healthy matched with expression "== true" and returned true
# String comparison expressions
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --path id --expression "== \"HeaFdiyIJe\""
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --query .id --expression "== \"HeaFdiyIJe\""
http-json OK: The value HeaFdiyIJe found at id matched with expression "== \"HeaFdiyIJe\"" and returned true
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --path id --expression "== \"BadText\""
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --query .id --expression "== \"BadText\""
http-json CRITICAL: The value HeaFdiyIJe found at id did not match with expression "== \"BadText\"" and returned false
# Numeric comparison expressions
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --path status --expression "== 200"
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --query .status --expression "== 200"
http-json OK: The value 200 found at status matched with expression "== 200" and returned true
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --path status --expression "< 300"
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --query .status --expression "< 300"
http-json OK: The value 200 found at status matched with expression "< 300" and returned true
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --path status --expression "> 300"
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --query .status --expression "> 300"
http-json CRITICAL: The value 200 found at status did not match with expression "> 300" and returned false
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --path status --expression "< 300" --header "Custom-Header: Custom header value"
# With a custom header
http-json --url https://icanhazdadjoke.com/j/HeaFdiyIJe --query .status --expression "< 300" --header "Custom-Header: Custom header value"
http-json OK: The value 200 found at status matched with expression "< 300" and returned true
```
Expand Down Expand Up @@ -311,3 +319,5 @@ For more information about contributing to this plugin, see [Contributing][4].
[3]: https://bonsai.sensu.io/assets/nixwiz/http-checks
[4]: https://github.com/sensu/sensu-go/blob/master/CONTRIBUTING.md
[5]: https://github.com/ncr-devops-platform/nagiosfoundation
[6]: https://github.com/stedolan/jq
[7]: https://github.com/itchyny/gojq
73 changes: 55 additions & 18 deletions cmd/http-json/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main

import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand All @@ -14,10 +15,9 @@ import (
"time"

"github.com/PaesslerAG/gval"
"github.com/itchyny/gojq"
"github.com/sensu-community/sensu-plugin-sdk/sensu"
corev2 "github.com/sensu/sensu-go/api/core/v2"
"github.com/sensu/sensu-go/types"
"github.com/thedevsaddam/gojsonq"
)

// Config represents the check plugin config.
Expand All @@ -27,7 +27,7 @@ type Config struct {
TrustedCAFile string
InsecureSkipVerify bool
Timeout int
Path string
Query string
Expression string
Headers []string
}
Expand Down Expand Up @@ -81,21 +81,21 @@ var (
Value: &plugin.Timeout,
},
{
Path: "path",
Path: "query",
Env: "",
Argument: "path",
Shorthand: "p",
Argument: "query",
Shorthand: "q",
Default: "",
Usage: "Path to query in JSON",
Value: &plugin.Path,
Usage: "Query written in jq format",
Value: &plugin.Query,
},
{
Path: "expression",
Env: "",
Argument: "expression",
Shorthand: "e",
Default: "",
Usage: "Expression to query in JSON",
Usage: "Expression for comparing result of query",
Value: &plugin.Expression,
},
{
Expand All @@ -115,7 +115,7 @@ func main() {
check.Execute()
}

func checkArgs(event *types.Event) (int, error) {
func checkArgs(event *corev2.Event) (int, error) {
if len(plugin.URL) == 0 {
return sensu.CheckStateWarning, fmt.Errorf("--url or CHECK_URL environment variable is required")
}
Expand All @@ -138,16 +138,16 @@ func checkArgs(event *types.Event) (int, error) {

tlsConfig.CipherSuites = corev2.DefaultCipherSuites

if len(plugin.Path) == 0 {
return sensu.CheckStateWarning, fmt.Errorf("--path is required")
if len(plugin.Query) == 0 {
return sensu.CheckStateWarning, fmt.Errorf("--query is required")
}
if len(plugin.Expression) == 0 {
return sensu.CheckStateWarning, fmt.Errorf("--expression is required")
}
return sensu.CheckStateOK, nil
}

func executeCheck(event *types.Event) (int, error) {
func executeCheck(event *corev2.Event) (int, error) {

client := http.DefaultClient
client.Transport = http.DefaultTransport
Expand Down Expand Up @@ -190,21 +190,58 @@ func executeCheck(event *types.Event) (int, error) {
return sensu.CheckStateCritical, err
}

value := gojsonq.New().JSONString(string(body)).Find(plugin.Path)
query, err := gojq.Parse(plugin.Query)
if err != nil {
return sensu.CheckStateCritical, fmt.Errorf("Failed to parse query %q, error: %v", plugin.Query, err)
}
code, err := gojq.Compile(query)
if err != nil {
return sensu.CheckStateCritical, fmt.Errorf("Failed to compile query %q, error: %v", plugin.Query, err)
}

var jsonBody interface{}

err = json.Unmarshal(body, &jsonBody)
if err != nil {
return sensu.CheckStateCritical, fmt.Errorf("Could not unmarshal response body into JSON: %v", err)
}

iter := code.Run(jsonBody)

var value interface{}

for {
var ok bool
v, ok := iter.Next()
if !ok {
// no more iterations
break
}
if _, ok := v.(error); ok {
// should we output anything here?
continue
}
value = v
}

if value == nil {
fmt.Printf("%s CRITICAL: No value was returned for query %q\n", plugin.PluginConfig.Name, plugin.Query)
return sensu.CheckStateCritical, nil
}

found, err := evaluateExpression(value, plugin.Expression, plugin.Path)
found, err := evaluateExpression(value, plugin.Expression)
if err != nil {
return sensu.CheckStateCritical, fmt.Errorf("Error evaluating expression: %v", err)
}
if found {
fmt.Printf("%s OK: The value %v found at %s matched with expression %q and returned true\n", plugin.PluginConfig.Name, value, plugin.Path, plugin.Expression)
fmt.Printf("%s OK: The value %v found at %s matched with expression %q and returned true\n", plugin.PluginConfig.Name, value, plugin.Query, plugin.Expression)
return sensu.CheckStateOK, nil
}

fmt.Printf("%s CRITICAL: The value %v found at %s did not match with expression %q and returned false\n", plugin.PluginConfig.Name, value, plugin.Path, plugin.Expression)
fmt.Printf("%s CRITICAL: The value %v found at %s did not match with expression %q and returned false\n", plugin.PluginConfig.Name, value, plugin.Query, plugin.Expression)
return sensu.CheckStateCritical, nil
}
func evaluateExpression(actualValue interface{}, expression, path string) (bool, error) {
func evaluateExpression(actualValue interface{}, expression string) (bool, error) {
evalResult, err := gval.Evaluate("value "+expression, map[string]interface{}{"value": actualValue})
if err != nil {
return false, err
Expand Down
30 changes: 15 additions & 15 deletions cmd/http-json/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,21 @@ func TestExecuteCheck(t *testing.T) {

testCases := []struct {
status int
path string
query string
expression string
}{
{sensu.CheckStateOK, "text", "== \"testing\""},
{sensu.CheckStateCritical, "text", "== \"notfound\""},
{sensu.CheckStateOK, "number", "== 10"},
{sensu.CheckStateCritical, "number", "== 11"},
{sensu.CheckStateOK, "number", ">= 10"},
{sensu.CheckStateOK, "number", "> 9"},
{sensu.CheckStateCritical, "number", ">= 11"},
{sensu.CheckStateCritical, "number", "> 12"},
{sensu.CheckStateOK, "number", "<= 10"},
{sensu.CheckStateOK, "number", "< 11"},
{sensu.CheckStateCritical, "number", "<= 9"},
{sensu.CheckStateCritical, "number", "< 8"},
{sensu.CheckStateOK, ".text", "== \"testing\""},
{sensu.CheckStateCritical, ".text", "== \"notfound\""},
{sensu.CheckStateOK, ".number", "== 10"},
{sensu.CheckStateCritical, ".number", "== 11"},
{sensu.CheckStateOK, ".number", ">= 10"},
{sensu.CheckStateOK, ".number", "> 9"},
{sensu.CheckStateCritical, ".number", ">= 11"},
{sensu.CheckStateCritical, ".number", "> 12"},
{sensu.CheckStateOK, ".number", "<= 10"},
{sensu.CheckStateOK, ".number", "< 11"},
{sensu.CheckStateCritical, ".number", "<= 9"},
{sensu.CheckStateCritical, ".number", "< 8"},
}

for _, tc := range testCases {
Expand All @@ -65,7 +65,7 @@ func TestExecuteCheck(t *testing.T) {
require.NoError(t, err)
plugin.URL = test.URL
plugin.Expression = tc.expression
plugin.Path = tc.path
plugin.Query = tc.query
status, err := executeCheck(event)
assert.NoError(err)
assert.Equal(tc.status, status)
Expand All @@ -82,7 +82,7 @@ func TestExecuteCheck(t *testing.T) {
_, err := url.ParseRequestURI(test.URL)
require.NoError(t, err)
plugin.URL = test.URL
plugin.Path = "number"
plugin.Query = ".number"
plugin.Expression = "== 10"
plugin.Headers = []string{"Test-Header-1: Test Header 1 Value", "Test-Header-2: Test Header 2 Value"}
status, err := executeCheck(event)
Expand Down
34 changes: 29 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,33 @@ module github.com/nixwiz/http-checks
go 1.13

require (
github.com/PaesslerAG/gval v1.0.1
github.com/sensu-community/sensu-plugin-sdk v0.6.0
github.com/sensu/sensu-go v0.0.0-20200131164840-40b1d5938251
github.com/stretchr/testify v1.4.0
github.com/thedevsaddam/gojsonq v2.3.0+incompatible
github.com/PaesslerAG/gval v1.1.0
github.com/coreos/etcd v3.3.25+incompatible // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/uuid v1.1.5 // indirect
github.com/itchyny/gojq v0.12.1
github.com/json-iterator/go v1.1.10 // indirect
github.com/magiconair/properties v1.8.4 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac // indirect
github.com/sensu-community/sensu-plugin-sdk v0.11.0
github.com/sensu/sensu-go/api/core/v2 v2.6.0
github.com/sensu/sensu-go/types v0.4.0
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.1.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.7.1 // indirect
github.com/stretchr/testify v1.6.1
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect
golang.org/x/text v0.3.5 // indirect
google.golang.org/genproto v0.0.0-20210120162456-f5e8c5e2aaf2 // indirect
google.golang.org/grpc v1.35.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
Loading

0 comments on commit c49d0c9

Please sign in to comment.