diff --git a/docs/checks/commands/check_temperature.md b/docs/checks/commands/check_temperature.md index 2157e86d..37338958 100644 --- a/docs/checks/commands/check_temperature.md +++ b/docs/checks/commands/check_temperature.md @@ -69,12 +69,13 @@ Naemon Config these can be used in filters and thresholds (along with the default attributes): -| Attribute | Description | -| --------- | ---------------------------------------------- | -| sensor | full name of this sensor, ex.: coretemp_core_0 | -| name | name of this sensor, ex.: coretemp | -| label | label for this sensor, ex.: core 0 | -| value | current temperature | -| crit | critical value supplied from sensor | -| max | max value supplied from sensor | -| min | min value supplied from sensor | +| Attribute | Description | +| ----------- | ------------------------------------------------------------------------------------------------------- | +| sensor | full name of this sensor, ex.: coretemp_core_0 | +| name | name of this sensor, ex.: coretemp | +| label | label for this sensor, ex.: core 0 | +| value | current temperature | +| crit | critical value supplied from sensor | +| max | max value supplied from sensor | +| min | min value supplied from sensor | +| temperature | raw temperature value. this can be a wrong/misleading value like 0 or 1 if sensor is disabled. use 'value' attribute instead. | diff --git a/pkg/snclient/check_drivesize.go b/pkg/snclient/check_drivesize.go index f1cc5bbe..aaeb3b3a 100644 --- a/pkg/snclient/check_drivesize.go +++ b/pkg/snclient/check_drivesize.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "maps" + "regexp" "runtime" "sort" "strconv" @@ -154,6 +155,11 @@ func (l *CheckDrivesize) Build() *CheckData { {name: "localised_remote_path", description: "Windows only: If the path is given as a remote path, and that remote path has an assigned logical drive," + " this is the replaced path under that logical drive."}, }, + extraFilterAttributes: []*regexp.Regexp{ + regexp.MustCompile(`^flags$`), + regexp.MustCompile(`^letter$`), + regexp.MustCompile(`^opts$`), + }, exampleDefault: l.getExample(), exampleArgs: `'warn=used_pct > 90' 'crit=used_pct > 95'`, } diff --git a/pkg/snclient/check_logfile.go b/pkg/snclient/check_logfile.go index 376278f4..7d043d11 100644 --- a/pkg/snclient/check_logfile.go +++ b/pkg/snclient/check_logfile.go @@ -85,6 +85,9 @@ func (c *CheckLogFile) Build() *CheckData { {name: "line", description: "Match the content of an entire line"}, {name: "columnN", description: "Match the content of the N-th column only if enough columns exists"}, }, + extraFilterAttributes: []*regexp.Regexp{ + regexp.MustCompile(`^column.*`), + }, exampleDefault: ` Alert if there are errors in the snclient log file: diff --git a/pkg/snclient/check_memory.go b/pkg/snclient/check_memory.go index e48ffc7e..30a24439 100644 --- a/pkg/snclient/check_memory.go +++ b/pkg/snclient/check_memory.go @@ -3,6 +3,7 @@ package snclient import ( "context" "fmt" + "regexp" "runtime" "github.com/consol-monitoring/snclient/pkg/humanize" @@ -76,6 +77,12 @@ read more on windows virtual address space: {name: "size", description: "Total memory in human readable bytes (IEC)", unit: UByte}, {name: "size_bytes", description: "Total memory in bytes (IEC)", unit: UByte}, }, + extraFilterAttributes: []*regexp.Regexp{ + regexp.MustCompile(`^(?i)physical$`), + regexp.MustCompile(`^(?i)swap$`), + regexp.MustCompile(`^(?i)committed$`), + regexp.MustCompile(`^(?i)virtual$`), + }, exampleDefault: ` check_memory OK - physical = 6.98 GiB, committed = 719.32 MiB|... diff --git a/pkg/snclient/check_os_updates.go b/pkg/snclient/check_os_updates.go index 49243ce6..c2146020 100644 --- a/pkg/snclient/check_os_updates.go +++ b/pkg/snclient/check_os_updates.go @@ -4,6 +4,7 @@ import ( "cmp" "context" "fmt" + "regexp" "slices" "github.com/consol-monitoring/snclient/pkg/convert" @@ -49,6 +50,12 @@ func (l *CheckOSUpdates) Build() *CheckData { {name: "security", description: "is this a security update: 0 / 1"}, {name: "version", description: "version string of package"}, }, + extraFilterAttributes: []*regexp.Regexp{ + regexp.MustCompile(`^old_version$`), + regexp.MustCompile(`^repository$`), + regexp.MustCompile(`^arch$`), + regexp.MustCompile(`^prefix$`), + }, exampleDefault: ` check_os_updates OK - no updates available |... diff --git a/pkg/snclient/check_pdh.go b/pkg/snclient/check_pdh.go index c2818e51..f5ad662e 100644 --- a/pkg/snclient/check_pdh.go +++ b/pkg/snclient/check_pdh.go @@ -2,6 +2,7 @@ package snclient import ( "context" + "regexp" ) func init() { @@ -48,6 +49,9 @@ func (c *CheckPDH) Build() *CheckData { {name: "count ", description: "Number of items matching the filter. Common option for all checks."}, {name: "value ", description: "The counter value (either float or int)"}, }, + extraFilterAttributes: []*regexp.Regexp{ + regexp.MustCompile(`^name$`), + }, exampleDefault: ` check_pdh "counter=foo" "warn=value > 80" "crit=value > 90" Everything looks good diff --git a/pkg/snclient/check_temperature.go b/pkg/snclient/check_temperature.go index 12dbc285..00c80dd3 100644 --- a/pkg/snclient/check_temperature.go +++ b/pkg/snclient/check_temperature.go @@ -57,6 +57,8 @@ func (l *CheckTemperature) Build() *CheckData { {name: "crit", description: "critical value supplied from sensor"}, {name: "max", description: "max value supplied from sensor"}, {name: "min", description: "min value supplied from sensor"}, + {name: "temperature", description: "raw temperature value." + + " this can be a wrong/misleading value like 0 or 1 if sensor is disabled. use 'value' attribute instead."}, }, listSorted: []string{"label"}, exampleDefault: ` diff --git a/pkg/snclient/check_wmi.go b/pkg/snclient/check_wmi.go index 80f007df..cca4628b 100644 --- a/pkg/snclient/check_wmi.go +++ b/pkg/snclient/check_wmi.go @@ -3,6 +3,7 @@ package snclient import ( "context" "fmt" + "regexp" "runtime" "strings" @@ -49,6 +50,9 @@ func (l *CheckWMI) Build() *CheckData { hasArgsFilter: true, // otherwise empty-syntax won't be applied topSyntax: "${list}", detailSyntax: "%(line)", + extraFilterAttributes: []*regexp.Regexp{ + regexp.MustCompile(`.*`), + }, exampleDefault: ` check_wmi "query=select DeviceID, FreeSpace FROM Win32_LogicalDisk WHERE DeviceID = 'C:'" C:, 27955118080 diff --git a/pkg/snclient/checkdata.go b/pkg/snclient/checkdata.go index f2303eef..101e514f 100644 --- a/pkg/snclient/checkdata.go +++ b/pkg/snclient/checkdata.go @@ -3,6 +3,7 @@ package snclient import ( "fmt" "math" + "regexp" "slices" "sort" "strconv" @@ -137,6 +138,7 @@ type CheckData struct { output OutputMode implemented Implemented attributes []CheckAttribute + extraFilterAttributes []*regexp.Regexp listSorted []string // sort result list by this keys exampleDefault string exampleArgs string @@ -858,6 +860,83 @@ func (cd *CheckData) preParseArgs(args []string) (sanitized []Argument, defaultW return sanitized, defaultWarning, defaultCritical, applyDefaultFilter, nil } +// Threshold keywords do not necessarily have to match an attribute name. +// They can be written to match perfdata metric names +// They can also be match to check details, but that is less common +// Therefore, this function does not return an error +func (cd *CheckData) checkThresholdKeywordsAgainstAttributeNames() { + attributeNames := cd.getAttributeNames() + + warningKeywords, err := cd.warnThreshold.GetListOfKeywords() + if err == nil && len(warningKeywords) > 0 { + warningKeywordsExtra := utils.SubtractSlice(warningKeywords, attributeNames) + if len(warningKeywordsExtra) > 0 { + log.Tracef("Warning condition uses keyword(s) not present in the attributes, run with --help to get a list of attributes, extra keywords: %v", warningKeywordsExtra) + } + } + + critKeywords, err := cd.critThreshold.GetListOfKeywords() + if err == nil && len(critKeywords) > 0 { + critKeywordsExtra := utils.SubtractSlice(critKeywords, attributeNames) + if len(critKeywordsExtra) > 0 { + log.Tracef("Crit condition uses keyword(s) not present in the attributes, run with --help to get a list of attributes, extra keywords: %v", critKeywordsExtra) + } + } + + okKeywords, err := cd.okThreshold.GetListOfKeywords() + if err == nil && len(okKeywords) > 0 { + okKeywordsExtra := utils.SubtractSlice(okKeywords, attributeNames) + if len(okKeywordsExtra) > 0 { + log.Tracef("Ok condition uses keyword(s) not present in the attributes, run with --help to get a list of attributes, extra keywords: %v", okKeywordsExtra) + } + } +} + +// Filter keywords should match attribute filters. +// They are used to filter entries added to the check. Entries use attributes. +// They are not compared with perfdata metric names. +// They are not compared with check details. +// Not matching an attribute name means the condition has no effect, and is likely wrong +func (cd *CheckData) checkFilterKeywordsAgainstAttributeNames() (err error) { + attributeNames := cd.getAttributeNames() + regexes := make([]*regexp.Regexp, 0, len(attributeNames)+len(cd.extraFilterAttributes)) + + for _, attributeName := range attributeNames { + regexes = append(regexes, regexp.MustCompile(regexp.QuoteMeta(attributeName))) + } + + regexes = append(regexes, cd.extraFilterAttributes...) + + filterKeywords, err := cd.filter.GetListOfKeywords() + + filterKeywordsUnmatched := []string{} + if err == nil && len(filterKeywords) > 0 { + for _, filterKeyword := range filterKeywords { + if !slices.ContainsFunc(regexes, func(r *regexp.Regexp) bool { return r.MatchString(filterKeyword) }) { + filterKeywordsUnmatched = append(filterKeywordsUnmatched, filterKeyword) + } + } + } + + if len(filterKeywordsUnmatched) > 0 { + log.Warnf("Filter condition uses keyword(s) not present in the attribute, filter condition: '%s' , extra keywords: '%s' ", cd.filter.String(), strings.Join(filterKeywordsUnmatched, ", ")) + + return fmt.Errorf("filter condition uses unknown attribute '%s', run with --help to get a list of attributes", filterKeywordsUnmatched[0]) + } + + return nil +} + +func (cd *CheckData) getAttributeNames() (attributeNames []string) { + attributeNames = make([]string, 0, len(cd.attributes)) + + for _, attribute := range cd.attributes { + attributeNames = append(attributeNames, attribute.name) + } + + return attributeNames +} + func (cd *CheckData) fetchNextArg(args, split []string, keyword string, idx, numArgs int) (argVal string, newIdx int, err error) { if len(split) == 2 { return split[1], idx, nil @@ -1122,15 +1201,18 @@ func (cd *CheckData) HasThreshold(name string) bool { // GetAllThresholdKeywords returns a list of all keywords used in warn/crit/ok thresholds. func (cd *CheckData) GetAllThresholdKeywords() []string { - keywords := []string{} + keywords := make([]string, 0, len(cd.warnThreshold)+len(cd.critThreshold)+len(cd.okThreshold)) - keywords = append(keywords, cd.getAllThresholdKeywords(cd.warnThreshold)...) - keywords = append(keywords, cd.getAllThresholdKeywords(cd.critThreshold)...) - keywords = append(keywords, cd.getAllThresholdKeywords(cd.okThreshold)...) + warnThresholdKeywords, _ := cd.warnThreshold.GetListOfKeywords() + critThresholdKeywords, _ := cd.critThreshold.GetListOfKeywords() + okThresholdKeywords, _ := cd.okThreshold.GetListOfKeywords() - // make list unique + keywords = append(keywords, warnThresholdKeywords...) + keywords = append(keywords, critThresholdKeywords...) + keywords = append(keywords, okThresholdKeywords...) + + utils.Deduplicate(keywords) slices.Sort(keywords) - keywords = slices.Compact(keywords) return keywords } @@ -1150,21 +1232,6 @@ func (cd *CheckData) hasThresholdCond(condList ConditionList, name string) bool return false } -// hasThresholdCond returns true is the given list of conditions uses the given name at least once. -func (cd *CheckData) getAllThresholdKeywords(condList ConditionList) []string { - keywords := []string{} - - for _, cond := range condList { - if len(cond.group) > 0 { - keywords = append(keywords, cd.getAllThresholdKeywords(cond.group)...) - } - - keywords = append(keywords, cond.keyword) - } - - return keywords -} - // SetDefaultThresholdUnit sets default unit for all threshold conditions matching // the name and not having a unit already func (cd *CheckData) SetDefaultThresholdUnit(defaultUnit string, names []string) { diff --git a/pkg/snclient/checkdata_test.go b/pkg/snclient/checkdata_test.go index 726faa46..0a540740 100644 --- a/pkg/snclient/checkdata_test.go +++ b/pkg/snclient/checkdata_test.go @@ -25,3 +25,30 @@ func TestAllRequiredMacros(t *testing.T) { macros = check.AllRequiredMacros() assert.Equal(t, []string{"blah", "column1", "column29", "test"}, macros) } + +func TestCheckingFilterKeywordsOnCheckAttributes(t *testing.T) { + check := &CheckData{ + name: "testcheck", + description: "This test check simply has couple arguments.", + attributes: []CheckAttribute{ + { + name: "attribute1", + description: "attribute1", + }, + { + name: "attribute2", + description: "attribute2", + }, + { + name: "attribute3", + description: "attribute3", + }, + }, + } + + _, err := check.parseArgs([]string{"filter='(attribute1 eq value1) and (attribute2 like value2) and (attribute3 != value3)'", "filter='attribute4 eq value4'"}) + require.NoError(t, err, "should not have a problem parsing arguments") + + err = check.checkFilterKeywordsAgainstAttributeNames() + require.Error(t, err, "should error due to filter using keyword 'attribute4', which is not an attribute of check") +} diff --git a/pkg/snclient/condition.go b/pkg/snclient/condition.go index 8fe92874..d7df3e43 100644 --- a/pkg/snclient/condition.go +++ b/pkg/snclient/condition.go @@ -992,7 +992,9 @@ func (c *Condition) TransformMultipleKeywords(srcKeywords []string, targetKeywor // recursively gets list of all keywords used in the condition func (c *Condition) GetListOfKeywords() (keywords []string, err error) { addKeywordToList := func(c *Condition) (err error) { - keywords = append(keywords, c.keyword) + if c.keyword != "" { + keywords = append(keywords, c.keyword) + } return nil } @@ -1532,3 +1534,20 @@ func (cl *ConditionList) performMatches(data map[string]string, returnOnFirstMat return correctConditions, falseConditions, conclusiveConditions, inconclusiveConditions } + +func (cl *ConditionList) GetListOfKeywords() (keywords []string, err error) { + keywords = make([]string, 0) + + for _, cond := range *cl { + condKeywords, err := cond.GetListOfKeywords() + if err != nil { + return nil, fmt.Errorf("error gathering keywords: %s", err.Error()) + } + + keywords = append(keywords, condKeywords...) + } + + keywords = utils.Deduplicate(keywords) + + return keywords, nil +} diff --git a/pkg/snclient/snclient.go b/pkg/snclient/snclient.go index 29383dc9..6dedfb8f 100644 --- a/pkg/snclient/snclient.go +++ b/pkg/snclient/snclient.go @@ -824,6 +824,7 @@ func (snc *Agent) runCheck(ctx context.Context, name string, args []string, time if chk.showHelp > 0 { return snc.runHelp(ctx, chk, handler), chk } + if !skipAllowedCheck { err = snc.checkAllowed(name, chk, handler, args, ArgumentList(parsedArgs).RawList(), transportConf) if err != nil { @@ -834,6 +835,17 @@ func (snc *Agent) runCheck(ctx context.Context, name string, args []string, time } } + if !chk.argsPassthrough { + chk.checkThresholdKeywordsAgainstAttributeNames() + err = chk.checkFilterKeywordsAgainstAttributeNames() + if err != nil { + return &CheckResult{ + State: CheckExitUnknown, + Output: fmt.Sprintf("${status} - %s", err.Error()), + }, chk + } + } + if timeoutOverride > 0 { chk.timeout = timeoutOverride }