diff --git a/docs/checks/plugins/check_dns.md b/docs/checks/plugins/check_dns.md index 0999fbf2..477cb7a6 100644 --- a/docs/checks/plugins/check_dns.md +++ b/docs/checks/plugins/check_dns.md @@ -52,14 +52,25 @@ Naemon Config check_dns [OPTIONS] Application Options: - -H, --host= The name or address you want to query - -s, --server= DNS server you want to use for the lookup - -p, --port= Port number you want to use (default: 53) - -q, --querytype= DNS record query type (default: A) - --norec Set not recursive mode - -e, --expected-string= IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at - once, you have to specify whole string + -H, --host= The name or address you want to query + -s, --server= DNS server you want to use for the lookup + -p, --port= Port number you want to use (default: 53) + -q, --querytype= DNS record query type (default: A) + --norec Set not recursive mode + -e, --expected-string= IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at + once, you have to specify whole string + --search-path= Search paths is added to the domains before sending a DNS query. This can be specified + multiple times. + --resolv-conf-file= Path to the resolv.conf file to use. Is not used in Windows. Default is /etc/resolv.conf . + (default: /etc/resolv.conf) + -v, --verbose Show verbose output. + -w, --warning= Warning timeout in seconds, if getting a successfull DNS query takes longer than specified, + set return status to warning. + -c, --critical= Critical timeout in seconds, if getting a successfull DNS query takes longer than specified, + set return status to critical. + -t, --timeout= If the program cannot get a successfull DNS response until the specified timeout in seconds, + it exits with critical status. (default: 10) Help Options: - -h, --help Show this help message + -h, --help Show this help message ``` diff --git a/pkg/check_dns/check_dns.go b/pkg/check_dns/check_dns.go index 993534f2..d7b22f98 100644 --- a/pkg/check_dns/check_dns.go +++ b/pkg/check_dns/check_dns.go @@ -6,9 +6,13 @@ import ( "fmt" "io" "net" + "runtime" + "slices" "strconv" "strings" + "time" + "github.com/consol-monitoring/snclient/pkg/utils" "github.com/mackerelio/checkers" "github.com/miekg/dns" "github.com/sni/go-flags" @@ -21,7 +25,7 @@ func Check(ctx context.Context, output io.Writer, args []string) int { return 2 } - ckr := opts.run() + ckr := opts.run(ctx) fmt.Fprintf(output, "%s - %s", ckr.Status, strings.TrimSpace(ckr.Message)) return int(ckr.Status) @@ -30,12 +34,18 @@ func Check(ctx context.Context, output io.Writer, args []string) int { // adopted from https://raw.githubusercontent.com/mackerelio/go-check-plugins/master/check-dns/lib/ // Apache-2.0 license type dnsOpts struct { - Host string `short:"H" long:"host" required:"true" description:"The name or address you want to query"` - Server string `short:"s" long:"server" description:"DNS server you want to use for the lookup"` - Port int `short:"p" long:"port" default:"53" description:"Port number you want to use"` - QueryType string `short:"q" long:"querytype" default:"A" description:"DNS record query type"` - Norec bool `long:"norec" description:"Set not recursive mode"` - ExpectedString []string `short:"e" long:"expected-string" description:"IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at once, you have to specify whole string"` + Host string `short:"H" long:"host" required:"true" description:"The name or address you want to query"` + Server string `short:"s" long:"server" description:"DNS server you want to use for the lookup"` + Port int `short:"p" long:"port" default:"53" description:"Port number you want to use"` + QueryType string `short:"q" long:"querytype" default:"A" description:"DNS record query type"` + Norec bool `long:"norec" description:"Set not recursive mode"` + ExpectedString []string `short:"e" long:"expected-string" description:"IP-ADDRESS string you expect the DNS server to return. If multiple IP-ADDRESS are returned at once, you have to specify whole string"` + SearchPaths []string `long:"search-path" description:"Search paths is added to the domains before sending a DNS query. This can be specified multiple times."` + ResolvConfFile string `long:"resolv-conf-file" default:"/etc/resolv.conf" description:"Path to the resolv.conf file to use. Is not used in Windows. Default is /etc/resolv.conf ."` + Verbose bool `short:"v" long:"vv" long:"vvv" long:"verbose" description:"Show verbose output."` + WarningTimeout *int `short:"w" long:"warning" description:"Warning timeout in seconds, if getting a successfull DNS query takes longer than specified, set return status to warning."` + CriticalTimeout *int `short:"c" long:"critical" description:"Critical timeout in seconds, if getting a successfull DNS query takes longer than specified, set return status to critical."` + Timeout int `short:"t" long:"timeout" default:"10" description:"If the program cannot get a successfull DNS response until the specified timeout in seconds, it exits with critical status."` } func parseArgs(args []string) (*dnsOpts, error) { @@ -46,18 +56,72 @@ func parseArgs(args []string) (*dnsOpts, error) { return opts, err } -func (opts *dnsOpts) run() *checkers.Checker { - var nameserver string +func (opts *dnsOpts) run(ctx context.Context) *checkers.Checker { var err error + var clientConfig *dns.ClientConfig + + logger := utils.LoggerFromContext(ctx) + tryLogTrace := func(logline string) { + if logger != nil && opts.Verbose { + logger.Trace(logline) + } + } + tryLogDebug := func(logline string) { + if logger != nil && opts.Verbose { + logger.Debug(logline) + } + } + + switch runtime.GOOS { + case "linux", "darwin", "freebsd": + clientConfig, err = dns.ClientConfigFromFile(opts.ResolvConfFile) + if err != nil { + return checkers.Critical(err.Error()) + } + default: + } + + var nameservers []string if opts.Server != "" { - nameserver = opts.Server + nameservers = []string{opts.Server} } else { - nameserver, err = adapterAddress() + nameservers, err = adapterAddress(clientConfig) if err != nil { return checkers.Critical(err.Error()) } } - nameserver = net.JoinHostPort(nameserver, strconv.Itoa(opts.Port)) + for i := range nameservers { + nameservers[i] = net.JoinHostPort(nameservers[i], strconv.Itoa(opts.Port)) + } + if logger != nil && opts.Verbose { + logger.Tracef("DNS nameservers: %v ", nameservers) + } + + var searchPaths []string + if len(opts.SearchPaths) > 0 { + searchPaths = opts.SearchPaths + } else { + searchPaths = getSearchPaths(clientConfig) + } + if logger != nil && opts.Verbose { + logger.Tracef("DNS search paths: %v ", searchPaths) + } + + var hostCandidates []string + originalHost := opts.Host + if dns.IsFqdn(originalHost) { + hostCandidates = append(hostCandidates, dns.Fqdn(originalHost)) + } else { + for _, searchPath := range searchPaths { + candidate := dns.Fqdn(originalHost + "." + searchPath) + hostCandidates = append(hostCandidates, candidate) + } + // try the bare host as FQDN as well without a searchPath + hostCandidates = append(hostCandidates, dns.Fqdn(originalHost)) + } + if logger != nil && opts.Verbose { + logger.Tracef("DNS host candidates: %v ", hostCandidates) + } queryType, ok := dns.StringToType[strings.ToUpper(opts.QueryType)] if !ok { @@ -65,21 +129,125 @@ func (opts *dnsOpts) run() *checkers.Checker { } c := new(dns.Client) - m := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - RecursionDesired: !opts.Norec, - Opcode: dns.OpcodeQuery, - }, - Question: []dns.Question{{Name: dns.Fqdn(opts.Host), Qtype: queryType, Qclass: dns.StringToClass["IN"]}}, + + var lastErr error + var r *dns.Msg + var duration time.Duration + + var successfulNameserver string + var successfulDuration time.Duration + var successfulHost string + + queryDNSChan := make(chan bool, 1) + queryDNSSuccessfull := false + dnsExchangeCount := 0 + + queryDNS := func() { + gotAnswer := false + + for _, hostCandidate := range hostCandidates { + for _, nameserver := range nameservers { + message := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + RecursionDesired: !opts.Norec, + Opcode: dns.OpcodeQuery, + }, + Question: []dns.Question{ + { + Name: hostCandidate, + Qtype: queryType, + Qclass: dns.StringToClass["IN"], + }, + }, + } + message.Id = dns.Id() + + r, duration, err = c.Exchange(message, nameserver) + dnsExchangeCount = dnsExchangeCount + 1 + + if err == nil { + if len(r.Answer) == 0 { + tryLogTrace(fmt.Sprintf("DNS query returned empty result, continuing to next combination, host: %s, nameserver: %s, duration: %dms", + hostCandidate, nameserver, duration.Milliseconds())) + + continue + } + + successfulNameserver = nameserver + successfulHost = hostCandidate + successfulDuration = duration + gotAnswer = true + tryLogDebug(fmt.Sprintf("successfully queried DNS, host: %s, nameserver: %s, duration: %dms", successfulHost, successfulNameserver, successfulDuration.Milliseconds())) + + break + } + + tryLogTrace(fmt.Sprintf("DNS query failed, host: %s, nameserver: %s, duration: %dms", hostCandidate, nameserver, duration.Milliseconds())) + + lastErr = err + } + + if gotAnswer { + break + } + } + + queryDNSChan <- gotAnswer } - m.Id = dns.Id() - r, _, err := c.Exchange(m, nameserver) - if err != nil { - return checkers.Critical(err.Error()) + queryBeginTimestamp := time.Now() + go queryDNS() + + select { + case <-time.After(time.Duration(opts.Timeout) * time.Second): + return checkers.Critical(fmt.Sprintf("Did not get a successfull DNS query in timeout: %d seconds", opts.Timeout)) + case queryDNSSuccessfull = <-queryDNSChan: + break + } + queryEndTimestamp := time.Now() + queryDuration := queryEndTimestamp.Sub(queryBeginTimestamp) + + if !queryDNSSuccessfull { + return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) + } + + if r == nil { + return checkers.Critical(fmt.Sprintf("All %d DNS queries gave empty results or failed, last error: %v", dnsExchangeCount, lastErr)) } checkSt := checkers.OK + + escalateStatus := func(newStatus checkers.Status) { + if newStatus > checkSt { + checkSt = newStatus + tryLogTrace(fmt.Sprintf("status escalated to %s", checkSt.String())) + } + } + + switch { + case opts.CriticalTimeout != nil && queryDuration.Seconds() > float64(*opts.CriticalTimeout): + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the critical threshold: %d", queryDuration.Seconds(), opts.CriticalTimeout)) + escalateStatus(checkers.CRITICAL) + case opts.WarningTimeout != nil && queryDuration.Seconds() > float64(*opts.WarningTimeout): + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, which is higher than the warning threshold: %d", queryDuration.Seconds(), opts.WarningTimeout)) + escalateStatus(checkers.WARNING) + default: + tryLogTrace(fmt.Sprintf("DNS query took %f seconds, and it is lower than (if specified) warning threshold: %v and critical threshold: %v", + queryDuration.Seconds(), opts.WarningTimeout, opts.CriticalTimeout)) + } + + answersWithoutHeaders := make([]string, 0) + answerTypes := make([]string, 0) + for _, answer := range r.Answer { + answerWithoutHeader, answerType, err := dnsAnswer(answer) + if err != nil { + return checkers.Critical(err.Error()) + } + answersWithoutHeaders = append(answersWithoutHeaders, answerWithoutHeader) + answerTypes = append(answerTypes, answerType) + } + + // Special handling of returned DNS addresses VS expected DNS addresses, with set comparisons /** if DNS server return 1.1.1.1, 2.2.2.2 1: -e 1.1.1.1 -e 2.2.2.2 -> OK @@ -95,48 +263,50 @@ func (opts *dnsOpts) run() *checkers.Checker { if !ok { return checkers.Critical(fmt.Sprintf("%s is not supported query type. Only A, AAAA, MX, CNAME are supported query types.", opts.QueryType)) } - match := 0 - for _, expectedString := range opts.ExpectedString { - for _, answer := range r.Answer { - anserWithoutHeader, _, err := dnsAnswer(answer) - if err != nil { - return checkers.Critical(err.Error()) - } - if anserWithoutHeader == expectedString { - match += 1 - } - } - } - if match == len(r.Answer) { - if len(opts.ExpectedString) == len(r.Answer) { // case 1 - checkSt = checkers.OK - } else { // case 2 - checkSt = checkers.WARNING - } - } else { - if match > 0 { // case 3,4 - checkSt = checkers.WARNING - } else { // case 5,6 - checkSt = checkers.CRITICAL - } + + expectedStringsContainOneAnswerAddress := slices.ContainsFunc(opts.ExpectedString, func(ex string) bool { + return slices.Contains(answersWithoutHeaders, ex) + }) + + answerCopy := slices.Clone(answersWithoutHeaders) + expectedCopy := slices.Clone(opts.ExpectedString) + slices.Sort(answerCopy) + slices.Sort(expectedCopy) + expectedStringsAndAnswersAreSame := slices.Equal(answerCopy, expectedCopy) + + switch { + case expectedStringsAndAnswersAreSame: + checkSt = checkers.OK + tryLogTrace(fmt.Sprintf("Expected strings: %v and strings from the DNS answer: %v , are the same", + opts.ExpectedString, answerCopy)) + case expectedStringsContainOneAnswerAddress: + tryLogTrace(fmt.Sprintf("Expected strings: %v contain one of the strings from the DNS answer: %v , but they are not the same, raising status to warning", + opts.ExpectedString, answerCopy)) + escalateStatus(checkers.WARNING) + case !expectedStringsContainOneAnswerAddress: + tryLogTrace(fmt.Sprintf("Expected strings: %v does not contain one of the strings from the DNS answer: %v , raising status to critical", + opts.ExpectedString, answerCopy)) + escalateStatus(checkers.CRITICAL) + default: + tryLogTrace(fmt.Sprintf("Could not comapre expected strings: %v with the strings from the DNS answer: %v , raising status to unknown", + opts.ExpectedString, answerCopy)) + escalateStatus(checkers.UNKNOWN) } } if r.MsgHdr.Rcode != dns.RcodeSuccess { - checkSt = checkers.CRITICAL + tryLogTrace("DNS does not have success return code, raising status to critical") + escalateStatus(checkers.CRITICAL) } msg := "" - if len(r.Answer) > 0 { - res, dnsType, err := dnsAnswer(r.Answer[0]) - if err != nil { - msg = err.Error() - } else { - msg = fmt.Sprintf("%s returns %s (%s)\n", opts.Host, res, dnsType) - } + if len(answersWithoutHeaders) > 0 && len(answerTypes) > 0 { + timeMetric := fmt.Sprintf("time=%fs;;", successfulDuration.Seconds()) + msg = fmt.Sprintf("%s returns %s (%s) |%s\n", opts.Host, answersWithoutHeaders[0], answerTypes[0], timeMetric) } else { - msg = fmt.Sprintf("%s (%s) returns no answer from %s\n", opts.Host, opts.QueryType, nameserver) + msg = fmt.Sprintf("%s (%s) returns no answer from %s\n", opts.Host, opts.QueryType, successfulNameserver) } + msg += fmt.Sprintf("HEADER-> %s\n", r.MsgHdr.String()) for _, answer := range r.Answer { msg += fmt.Sprintf("ANSWER-> %s\n", answer) diff --git a/pkg/check_dns/nameserver.go b/pkg/check_dns/nameserver.go index 849d7e21..ebc9a974 100644 --- a/pkg/check_dns/nameserver.go +++ b/pkg/check_dns/nameserver.go @@ -10,28 +10,47 @@ import ( "github.com/miekg/dns" ) -func adapterAddress() (string, error) { - conf, err := dns.ClientConfigFromFile("/etc/resolv.conf") - if err != nil { - return "", err - } +func adapterAddress(conf *dns.ClientConfig) (nameservers []string, err error) { if len(conf.Servers) == 0 { - return "", fmt.Errorf("no valid nameserver found") + return nameservers, fmt.Errorf("no valid nameserver found") } - nameserver := conf.Servers[0] - // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L148-L153 - // if the nameserver is from /etc/resolv.conf the [ and ] are already - // added, thereby breaking net.ParseIP. Check for this and don't - // fully qualify such a name - if nameserver[0] == '[' && nameserver[len(nameserver)-1] == ']' { - nameserver = nameserver[1 : len(nameserver)-1] + + parseNameserver := func(nameserver string) (string, error) { + // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L148-L153 + // if the nameserver is from /etc/resolv.conf the [ and ] are already + // added, thereby breaking net.ParseIP. Check for this and don't + // fully qualify such a name + if nameserver[0] == '[' && nameserver[len(nameserver)-1] == ']' { + nameserver = nameserver[1 : len(nameserver)-1] + } + + // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L154-L158 + if net.ParseIP(nameserver) == nil { + nameserver = dns.Fqdn(nameserver) + } + if net.ParseIP(nameserver) == nil { + return "", fmt.Errorf("invalid nameserver: %s", nameserver) + } + + return nameserver, nil } - // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L154-L158 - if net.ParseIP(nameserver) == nil { - nameserver = dns.Fqdn(nameserver) + + parsedNameservers := make([]string, 0, len(conf.Servers)) + for _, nameserver := range conf.Servers { + parsedNameserver, err := parseNameserver(nameserver) + + if err == nil { + parsedNameservers = append(parsedNameservers, parsedNameserver) + } } - if net.ParseIP(nameserver) == nil { - return "", fmt.Errorf("invalid nameserver: %s", nameserver) + + if len(parsedNameservers) == 0 { + return nameservers, fmt.Errorf("could not parse any nameserver") } - return nameserver, nil + + return parsedNameservers, nil +} + +func getSearchPaths(conf *dns.ClientConfig) []string { + return conf.Search } diff --git a/pkg/check_dns/nameserver_windows.go b/pkg/check_dns/nameserver_windows.go index a45d1fe0..7a287039 100644 --- a/pkg/check_dns/nameserver_windows.go +++ b/pkg/check_dns/nameserver_windows.go @@ -15,7 +15,9 @@ import ( ) // ref: https://go.dev/src/net/interface_windows.go -func adapterAddress() (string, error) { +// windows does not use a resolv.config file i.e dns.ClientConfig +// this function is defined for consistency across platforms +func adapterAddress(_ *dns.ClientConfig) (nameservers []string, err error) { var b []byte l := uint32(15000) // recommended initial size for { @@ -23,15 +25,15 @@ func adapterAddress() (string, error) { err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) if err == nil { if l == 0 { - return "", nil + return nameservers, nil } break } if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { - return "", os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) + return nameservers, os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) } if l <= uint32(len(b)) { - return "", os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) + return nameservers, os.NewSyscallError("syscall failed: GetAdaptersAddresses", err) } } var aas []*windows.IpAdapterAddresses @@ -39,7 +41,7 @@ func adapterAddress() (string, error) { aas = append(aas, aa) } if len(aas) == 0 { - return "", fmt.Errorf("no valid nameserver found") + return nameservers, fmt.Errorf("no valid nameserver found") } nameserver := aas[0].FirstDnsServerAddress.Address.IP().String() // ref: https://github.com/miekg/exdns/blob/d851fa434ad51cb84500b3e18b8aa7d3bead2c51/q/q.go#L154-L158 @@ -47,7 +49,11 @@ func adapterAddress() (string, error) { nameserver = dns.Fqdn(nameserver) } if net.ParseIP(nameserver) == nil { - return "", fmt.Errorf("invalid nameserver: %s", nameserver) + return nameservers, fmt.Errorf("invalid nameserver: %s", nameserver) } - return nameserver, nil + return []string{nameserver}, nil +} + +func getSearchPaths(_ *dns.ClientConfig) []string { + return []string{} } diff --git a/pkg/snclient/builtin_check.go b/pkg/snclient/builtin_check.go index 00e0122f..c98452b1 100644 --- a/pkg/snclient/builtin_check.go +++ b/pkg/snclient/builtin_check.go @@ -8,6 +8,8 @@ import ( "os" "regexp" "strings" + + "github.com/consol-monitoring/snclient/pkg/utils" ) type CheckBuiltin struct { @@ -59,12 +61,17 @@ func (l *CheckBuiltin) Check(ctx context.Context, snc *Agent, check *CheckData, switch { case snc.flags.Verbose >= 3: args = append(args, "-vvv") + log.Tracef("adding -vvv to the builtin check arguments") case snc.flags.Verbose >= 2: args = append(args, "-vv") + log.Tracef("adding -vv to the builtin check arguments") case snc.flags.Verbose >= 1: args = append(args, "-v") + log.Tracef("adding -v to the builtin check arguments") } + ctx = utils.ContextWithLogger(ctx, log) + output := bytes.NewBuffer(nil) rc := l.check(ctx, output, args) check.result.Output = output.String() diff --git a/pkg/snclient/check_dns_test.go b/pkg/snclient/check_dns_test.go index e81cc618..a80779b1 100644 --- a/pkg/snclient/check_dns_test.go +++ b/pkg/snclient/check_dns_test.go @@ -15,14 +15,141 @@ CheckBuiltinPlugins = enabled ` snc := StartTestAgent(t, config) - res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de"}) - assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf( - t, - `^OK - labs\.consol\.de returns 94\.185\.89\.33`, - string(res.BuildPluginOutput()), - "output matches", - ) + t.Run("basic a lookup", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string all match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "94.185.89.33"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string extra expected", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "94.185.89.33", "-e", "1.2.3.4"}) + assert.Equalf(t, CheckExitWarning, res.State, "state warning") + assert.Regexpf( + t, + `^WARNING - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string missing expected", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "94.185.89.33", "-e", "1.2.3.4", "-e", "5.6.7.8"}) + assert.Equalf(t, CheckExitWarning, res.State, "state warning") + assert.Regexpf( + t, + `^WARNING - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("expected string none match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-e", "1.2.3.4"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - labs\.consol\.de returns 94\.185\.89\.33`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("warning threshold not triggered", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-w", "999"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK`, + string(res.BuildPluginOutput()), + "not warned", + ) + }) + + t.Run("critical threshold triggered", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-c", "0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL`, + string(res.BuildPluginOutput()), + "critical threshold triggered", + ) + }) + + t.Run("aaaa lookup", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-q", "AAAA"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 2a03:3680:0:2::21 \(AAAA\)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("aaaa expected string match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "labs.consol.de", "-q", "AAAA", "-e", "2a03:3680:0:2::21"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - labs\.consol\.de returns 2a03:3680:0:2::21`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + t.Run("multiple answers all match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "one.one.one.one", "-e", "1.1.1.1", "-e", "1.0.0.1"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^OK - one\.one\.one\.one returns (1\.1\.1\.1|1\.0\.0\.1)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + // Resolves cloudflares one.one.one.one using whatever nameserver is configured, not the 1.1.1.1 DNS namesever + + t.Run("multiple answers partial match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "one.one.one.one", "-e", "1.1.1.1"}) + assert.Equalf(t, CheckExitWarning, res.State, "state warning") + assert.Regexpf( + t, + `^WARNING - one\.one\.one\.one returns (1\.1\.1\.1|1\.0\.0\.1)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) + + // Resolves cloudflares one.one.one.one using whatever nameserver is configured, not the 1.1.1.1 DNS namesever + + t.Run("multiple answers none match", func(t *testing.T) { + res := snc.RunCheck("check_dns", []string{"-H", "one.one.one.one", "-e", "8.8.8.8"}) + assert.Equalf(t, CheckExitCritical, res.State, "state critical") + assert.Regexpf( + t, + `^CRITICAL - one\.one\.one\.one returns (1\.1\.1\.1|1\.0\.0\.1)`, + string(res.BuildPluginOutput()), + "output matches", + ) + }) StopTestAgent(t, snc) } diff --git a/pkg/utils/context.go b/pkg/utils/context.go new file mode 100644 index 00000000..408f4963 --- /dev/null +++ b/pkg/utils/context.go @@ -0,0 +1,22 @@ +package utils + +import ( + "context" + + "github.com/kdar/factorlog" +) + +type loggerCtxKey struct{} + +//nolint:ireturn // context helpers must return context.Context by convention +func ContextWithLogger(ctx context.Context, log *factorlog.FactorLog) context.Context { + return context.WithValue(ctx, loggerCtxKey{}, log) +} + +func LoggerFromContext(ctx context.Context) *factorlog.FactorLog { + if log, ok := ctx.Value(loggerCtxKey{}).(*factorlog.FactorLog); ok { + return log + } + + return nil +}