diff --git a/README.md b/README.md index 2bf5eab6..a8e6fda2 100644 --- a/README.md +++ b/README.md @@ -568,10 +568,17 @@ http: # Response Checking contain: "success" # response body must contain this string, if not the probe is considered failed. not_contain: "failure" # response body must NOT contain this string, if it does the probe is considered failed. + regex: false # if true, the contain and not_contain will be treated as regular expression. default: false # configuration timeout: 10s # default is 30 seconds ``` + +> **Note**: +> +> The Regular Expression supported refer to https://github.com/google/re2/wiki/Syntax + + ### 3.2 TCP Probe Configuration ```YAML @@ -616,6 +623,8 @@ shell: - "REDISCLI_AUTH=abc123" # check the command output, if does not contain the PONG, mark the status down contain : "PONG" + not_contain: "failure" # response body must NOT contain this string, if it does the probe is considered failed. + regex: false # if true, the `contain` and `not_contain` will be treated as regular expression. default: false # Run Zookeeper command `stat` to check the zookeeper status - name: Zookeeper (Local) @@ -626,6 +635,10 @@ shell: contain: "Mode:" ``` +> **Note**: +> +> The Regular Expression supported refer to https://github.com/google/re2/wiki/Syntax + ### 3.4 SSH Command Probe Configuration SSH probe is similar to Shell probe. @@ -670,6 +683,8 @@ ssh: - "REDISCLI_AUTH=abc123" # check the command output, if does not contain the PONG, mark the status down contain : "PONG" + not_contain: "failure" # response body must NOT contain this string, if it does the probe is considered failed. + regex: false # if true, the contain and not_contain will be treated as regular expression. default: false # Check the process status of `Kafka` - name: Kafka (GCP) @@ -679,6 +694,9 @@ ssh: key: /path/to/private.key cmd: "ps -ef | grep kafka" ``` +> **Note**: +> +> The Regular Expression supported refer to https://github.com/google/re2/wiki/Syntax ### 3.5 TLS Probe Configuration diff --git a/probe/common.go b/probe/common.go index 9786b370..fa36730c 100644 --- a/probe/common.go +++ b/probe/common.go @@ -19,20 +19,84 @@ package probe import ( "fmt" + "regexp" "strings" ) -// CheckOutput checks the output text, +// TextChecker is the struct to check the output +type TextChecker struct { + Contain string `yaml:"contain,omitempty"` + NotContain string `yaml:"not_contain,omitempty"` + RegExp bool `yaml:"regex,omitempty"` + + containReg *regexp.Regexp `yaml:"-"` + notContainReg *regexp.Regexp `yaml:"-"` +} + +// Config the text checker initialize the regexp +func (tc *TextChecker) Config() (err error) { + if !tc.RegExp { + return nil + } + + if len(tc.Contain) == 0 { + tc.containReg = nil + } else if tc.containReg, err = regexp.Compile(tc.Contain); err != nil { + tc.containReg = nil + return err + } + + if len(tc.NotContain) == 0 { + tc.notContainReg = nil + } else if tc.notContainReg, err = regexp.Compile(tc.NotContain); err != nil { + tc.notContainReg = nil + return err + } + + return nil +} + +// Check the text +func (tc *TextChecker) Check(Text string) error { + if tc.RegExp { + return tc.CheckRegExp(Text) + } + return tc.CheckText(Text) +} + +func (tc *TextChecker) String() string { + if tc.RegExp { + return fmt.Sprintf("RegExp Mode - Contain:[%s], NotContain:[%s]", tc.Contain, tc.NotContain) + } + return fmt.Sprintf("Text Mode - Contain:[%s], NotContain:[%s]", tc.Contain, tc.NotContain) +} + +// CheckText checks the output text, // - if it contains a configured string then return nil // - if it does not contain a configured string then return nil -func CheckOutput(Contain, NotContain, Output string) error { +func (tc *TextChecker) CheckText(Output string) error { + + if len(tc.Contain) > 0 && !strings.Contains(Output, tc.Contain) { + return fmt.Errorf("the output does not contain [%s]", tc.Contain) + } + + if len(tc.NotContain) > 0 && strings.Contains(Output, tc.NotContain) { + return fmt.Errorf("the output contains [%s]", tc.NotContain) + } + return nil +} + +// CheckRegExp checks the output text, +// - if it contains a configured pattern then return nil +// - if it does not contain a configured pattern then return nil +func (tc *TextChecker) CheckRegExp(Output string) error { - if len(Contain) > 0 && !strings.Contains(Output, Contain) { - return fmt.Errorf("the output does not contain [%s]", Contain) + if len(tc.Contain) > 0 && tc.containReg != nil && !tc.containReg.MatchString(Output) { + return fmt.Errorf("the output does not match the pattern [%s]", tc.Contain) } - if len(NotContain) > 0 && strings.Contains(Output, NotContain) { - return fmt.Errorf("the output contains [%s]", NotContain) + if len(tc.NotContain) > 0 && tc.notContainReg != nil && tc.notContainReg.MatchString(Output) { + return fmt.Errorf("the output match the pattern [%s]", tc.NotContain) } return nil } diff --git a/probe/common_test.go b/probe/common_test.go index ceb034f4..bc9183eb 100644 --- a/probe/common_test.go +++ b/probe/common_test.go @@ -23,20 +23,130 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCheckOutput(t *testing.T) { - err := CheckOutput("hello", "good", "easeprobe hello world") +func TestCheckText(t *testing.T) { + tc := TextChecker{} + + tc.Contain = "hello" + tc.NotContain = "bad" + err := tc.Check("easeprobe hello world") assert.Nil(t, err) - err = CheckOutput("hello", "world", "easeprobe hello world") + tc.Contain = "hello" + tc.NotContain = "world" + err = tc.Check("easeprobe hello world") assert.NotNil(t, err) - err = CheckOutput("hello", "world", "easeprobe hello world") + tc.Contain = "" + tc.NotContain = "world" + err = tc.Check("easeprobe hello world") assert.NotNil(t, err) - err = CheckOutput("good", "bad", "easeprobe hello world") + tc.Contain = "hello" + tc.NotContain = "" + err = tc.Check("easeprobe hello world") + assert.Nil(t, err) + + tc.Contain = "good" + tc.NotContain = "" + err = tc.Check("easeprobe hello world") + assert.NotNil(t, err) + + tc.Contain = "" + tc.NotContain = "bad" + err = tc.Check("easeprobe hello world") + assert.Nil(t, err) + + tc.Contain = "good" + tc.NotContain = "bad" + err = tc.Check("easeprobe hello world") assert.NotNil(t, err) } +func testRegExpHelper(t *testing.T, regExp string, str string, match bool) { + tc := TextChecker{RegExp: true} + tc.Contain = regExp + tc.Config() + if match { + assert.Nil(t, tc.CheckRegExp(str)) + } else { + assert.NotNil(t, tc.CheckRegExp(str)) + } + + tc.Contain = "" + tc.NotContain = regExp + tc.Config() + if match { + assert.NotNil(t, tc.CheckRegExp(str)) + } else { + assert.Nil(t, tc.CheckRegExp(str)) + } +} + +func TestCheckRegExp(t *testing.T) { + + word := `word[0-9]+` + testRegExpHelper(t, word, "word word10 word", true) + testRegExpHelper(t, word, "word word word", false) + + time := "[0-9]?[0-9]:[0-9][0-9]" + testRegExpHelper(t, time, "easeprobe hello world 12:34", true) + testRegExpHelper(t, time, "easeprobe hello world 1234", false) + + html := `<\/?[\w\s]*>|<.+[\W]>` + testRegExpHelper(t, html, "

test hello world

", true) + testRegExpHelper(t, html, "test hello world", false) + + or := `word1|word2` + testRegExpHelper(t, or, "word1 easeprobe word2", true) + testRegExpHelper(t, or, "word2 easeprobe word1", true) + testRegExpHelper(t, or, "word3 easeprobe word1", true) + testRegExpHelper(t, or, "word2 easeprobe word3", true) + testRegExpHelper(t, or, "word easeprobe word3", false) + testRegExpHelper(t, or, "word easeprobe hello world", false) + + unsupported := "(?=.*word1)(?=.*word2)" + tc := TextChecker{RegExp: true} + tc.Contain = unsupported + err := tc.Config() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid or unsupported Perl syntax") + + tc.Contain = "" + tc.NotContain = unsupported + err = tc.Config() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid or unsupported Perl syntax") + +} + +func TestTextChecker(t *testing.T) { + checker := TextChecker{ + Contain: "hello", + NotContain: "", + RegExp: false, + } + checker.Config() + assert.Nil(t, checker.Check("hello world")) + assert.Contains(t, checker.String(), "Text Mode") + + checker = TextChecker{ + Contain: "[0-9]+$", + NotContain: "", + RegExp: true, + } + checker.Config() + assert.Nil(t, checker.Check("hello world 2022")) + assert.Contains(t, checker.String(), "RegExp Mode") + + checker = TextChecker{ + Contain: "", + NotContain: `<\/?[\w\s]*>|<.+[\W]>`, + RegExp: true, + } + checker.Config() + assert.NotNil(t, checker.Check("

test hello world

")) +} + func TestCheckEmpty(t *testing.T) { assert.Equal(t, "a", CheckEmpty("a")) assert.Equal(t, "empty", CheckEmpty(" ")) diff --git a/probe/http/http.go b/probe/http/http.go index bbaee04b..3682956e 100644 --- a/probe/http/http.go +++ b/probe/http/http.go @@ -45,8 +45,9 @@ type HTTP struct { Method string `yaml:"method,omitempty"` Headers map[string]string `yaml:"headers,omitempty"` Body string `yaml:"body,omitempty"` - Contain string `yaml:"contain,omitempty"` - NotContain string `yaml:"not_contain,omitempty"` + + // Output Text Checker + probe.TextChecker `yaml:",inline"` // Option - HTTP Basic Auth Credentials User string `yaml:"username,omitempty"` @@ -136,6 +137,10 @@ func (h *HTTP) Config(gConf global.ProbeSettings) error { } h.SuccessCode = codeRange + if err := h.TextChecker.Config(); err != nil { + return err + } + h.metrics = newMetrics(kind, tag) log.Debugf("[%s / %s] configuration: %+v", h.ProbeKind, h.ProbeName, h) @@ -197,7 +202,9 @@ func (h *HTTP) DoProbe() (bool, string) { } message := fmt.Sprintf("HTTP Status Code is %d", resp.StatusCode) - if err := probe.CheckOutput(h.Contain, h.NotContain, string(response)); err != nil { + + log.Debugf("[%s / %s] - %s", h.ProbeKind, h.ProbeName, h.TextChecker.String()) + if err := h.Check(string(response)); err != nil { log.Errorf("[%s / %s] - %v", h.ProbeKind, h.ProbeName, err) message += fmt.Sprintf(". Error: %v", err) return false, message diff --git a/probe/http/http_test.go b/probe/http/http_test.go index 52d40ee5..41df2169 100644 --- a/probe/http/http_test.go +++ b/probe/http/http_test.go @@ -30,6 +30,7 @@ import ( "bou.ke/monkey" "github.com/megaease/easeprobe/global" + "github.com/megaease/easeprobe/probe" "github.com/megaease/easeprobe/probe/base" "github.com/stretchr/testify/assert" ) @@ -41,10 +42,12 @@ func createHTTP() *HTTP { ContentEncoding: "text/json", Headers: map[string]string{"header1": "value1", "header2": "value2"}, Body: "{ \"key1\": \"value1\", \"key2\": \"value2\" }", - Contain: "good", - NotContain: "bad", - User: "user", - Pass: "pass", + TextChecker: probe.TextChecker{ + Contain: "good", + NotContain: "bad", + }, + User: "user", + Pass: "pass", TLS: global.TLS{ CA: "ca.crt", Cert: "cert.crt", @@ -82,6 +85,29 @@ func TestHTTPConfig(t *testing.T) { monkey.UnpatchAll() } +func TestTextCheckerConfig(t *testing.T) { + h := createHTTP() + h.TextChecker = probe.TextChecker{ + Contain: "", + NotContain: "", + RegExp: true, + } + h.TLS = global.TLS{} + + err := h.Config(global.ProbeSettings{}) + assert.NoError(t, err) + + h.Contain = `[a-zA-z]\d+` + err = h.Config(global.ProbeSettings{}) + assert.NoError(t, err) + assert.Equal(t, `[a-zA-z]\d+`, h.TextChecker.Contain) + + h.NotContain = `(?=.*word1)(?=.*word2)` + err = h.Config(global.ProbeSettings{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid or unsupported Perl syntax") +} + func TestHTTPDoProbe(t *testing.T) { // clear request h := createHTTP() diff --git a/probe/shell/shell.go b/probe/shell/shell.go index 06b4e87a..38eaa5dc 100644 --- a/probe/shell/shell.go +++ b/probe/shell/shell.go @@ -37,8 +37,9 @@ type Shell struct { Args []string `yaml:"args,omitempty"` Env []string `yaml:"env,omitempty"` CleanEnv bool `yaml:"clean_env,omitempty"` - Contain string `yaml:"contain,omitempty"` - NotContain string `yaml:"not_contain,omitempty"` + + // Output Text Checker + probe.TextChecker `yaml:",inline"` exitCode int `yaml:"-"` outputLen int `yaml:"-"` @@ -54,6 +55,10 @@ func (s *Shell) Config(gConf global.ProbeSettings) error { s.DefaultProbe.Config(gConf, kind, tag, name, global.CommandLine(s.Command, s.Args), s.DoProbe) + if err := s.TextChecker.Config(); err != nil { + return err + } + s.metrics = newMetrics(kind, tag) log.Debugf("[%s / %s] configuration: %+v, %+v", s.ProbeKind, s.ProbeName, s, s.Result()) @@ -97,7 +102,8 @@ func (s *Shell) DoProbe() (bool, string) { s.ExportMetrics() - if err := probe.CheckOutput(s.Contain, s.NotContain, string(output)); err != nil { + log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, s.TextChecker.String()) + if err := s.Check(string(output)); err != nil { log.Errorf("[%s / %s] - %v", s.ProbeKind, s.ProbeName, err) message = fmt.Sprintf("Error: %v", err) status = false diff --git a/probe/shell/shell_test.go b/probe/shell/shell_test.go index 09c0f339..028a003b 100644 --- a/probe/shell/shell_test.go +++ b/probe/shell/shell_test.go @@ -26,6 +26,7 @@ import ( "bou.ke/monkey" "github.com/megaease/easeprobe/global" + "github.com/megaease/easeprobe/probe" "github.com/megaease/easeprobe/probe/base" "github.com/stretchr/testify/assert" ) @@ -36,10 +37,35 @@ func createShell() *Shell { Command: "dummy command", Args: []string{"arg1", "arg2"}, Env: []string{"env1=value1", "env2=value2"}, - Contain: "good", - NotContain: "bad", + TextChecker: probe.TextChecker{ + Contain: "good", + NotContain: "bad", + }, } } + +func TestTextCheckerConfig(t *testing.T) { + s := createShell() + s.TextChecker = probe.TextChecker{ + Contain: "", + NotContain: "", + RegExp: true, + } + + err := s.Config(global.ProbeSettings{}) + assert.NoError(t, err) + + s.Contain = `[a-zA-z]\d+` + err = s.Config(global.ProbeSettings{}) + assert.NoError(t, err) + assert.Equal(t, `[a-zA-z]\d+`, s.TextChecker.Contain) + + s.NotContain = `(?=.*word1)(?=.*word2)` + err = s.Config(global.ProbeSettings{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid or unsupported Perl syntax") +} + func TestShell(t *testing.T) { s := createShell() s.Config(global.ProbeSettings{}) diff --git a/probe/ssh/ssh.go b/probe/ssh/ssh.go index 89e3ab69..da061fb8 100644 --- a/probe/ssh/ssh.go +++ b/probe/ssh/ssh.go @@ -41,8 +41,9 @@ type Server struct { Command string `yaml:"cmd"` Args []string `yaml:"args,omitempty"` Env []string `yaml:"env,omitempty"` - Contain string `yaml:"contain,omitempty"` - NotContain string `yaml:"not_contain,omitempty"` + + // Output Text Checker + probe.TextChecker `yaml:",inline"` BastionID string `yaml:"bastion"` bastion *Endpoint `yaml:"-"` @@ -116,6 +117,10 @@ func (s *Server) Configure(gConf global.ProbeSettings, return err } + if err := s.TextChecker.Config(); err != nil { + return err + } + log.Debugf("[%s / %s] configuration: %+v", s.ProbeKind, s.ProbeName, s) return nil } @@ -142,7 +147,8 @@ func (s *Server) DoProbe() (bool, string) { status = false message = err.Error() + " - " + output } else { - if err := probe.CheckOutput(s.Contain, s.NotContain, string(output)); err != nil { + log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, s.TextChecker.String()) + if err := s.Check(string(output)); err != nil { log.Errorf("[%s / %s] - %v", s.ProbeKind, s.ProbeName, err) message = fmt.Sprintf("Error: %v", err) status = false diff --git a/probe/ssh/ssh_test.go b/probe/ssh/ssh_test.go index d535a6b9..3ce726bd 100644 --- a/probe/ssh/ssh_test.go +++ b/probe/ssh/ssh_test.go @@ -27,6 +27,7 @@ import ( "bou.ke/monkey" "github.com/megaease/easeprobe/global" + "github.com/megaease/easeprobe/probe" "github.com/megaease/easeprobe/probe/base" "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh" @@ -65,12 +66,14 @@ func createSSHConfig() *SSH { Password: "", client: &ssh.Client{}, }, - Command: "test", - Args: []string{}, - Env: []string{}, - Contain: "good", - NotContain: "bad", - BastionID: "aws", + Command: "test", + Args: []string{}, + Env: []string{}, + TextChecker: probe.TextChecker{ + Contain: "good", + NotContain: "bad", + }, + BastionID: "aws", }, { DefaultProbe: base.DefaultProbe{ @@ -91,6 +94,41 @@ func createSSHConfig() *SSH { }, } } +func TestErrorServerConfig(t *testing.T) { + ssh := createSSHConfig() + s := ssh.Servers[0] + + s.Host = "asdf:asdf:22" + err := s.Config(global.ProbeSettings{}) + assert.Error(t, err) + + s.Password = "" + s.PrivateKey = "" + err = s.Config(global.ProbeSettings{}) + assert.Error(t, err) +} +func TestTextCheckerConfig(t *testing.T) { + ssh := createSSHConfig() + s := ssh.Servers[0] + s.TextChecker = probe.TextChecker{ + Contain: "", + NotContain: "", + RegExp: true, + } + + err := s.Config(global.ProbeSettings{}) + assert.NoError(t, err) + + s.Contain = `[a-zA-z]\d+` + err = s.Config(global.ProbeSettings{}) + assert.NoError(t, err) + assert.Equal(t, `[a-zA-z]\d+`, s.TextChecker.Contain) + + s.NotContain = `(?=.*word1)(?=.*word2)` + err = s.Config(global.ProbeSettings{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid or unsupported Perl syntax") +} func TestSSH(t *testing.T) { _ssh := createSSHConfig()