Add check_multi - #451
Conversation
|
Edit: Done |
There was a problem hiding this comment.
Pull request overview
Adds a new check_multi command that can run multiple checks (inline or from config sections) and aggregate status/output/perfdata, along with config and documentation updates to support it.
Changes:
- Introduce new
pkg/snclient/check_multi.gocheck implementation plus comprehensive tests. - Extend INI parsing/serialization to allow “bare lines” in
/settings/check/multi/*sections (raw command/script lines). - Enhance list-macro generation in
CheckData(unknown counts, warning/critical aliases) and adjust an existingcheck_filesexpectation accordingly.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds check_multi to the documented command list. |
| pkg/snclient/config.go | Allows raw/bare command lines in /settings/check/multi/* INI sections and serializes them without =. |
| pkg/snclient/checkdata.go | Adds unknown/warning/critical list macros and disables filter for checks that opt out. |
| pkg/snclient/check_multi.go | Implements check_multi execution/aggregation (inline + config modes). |
| pkg/snclient/check_multi_test.go | Adds test coverage for inline/config modes, limits, disabling, and check_index visibility. |
| pkg/snclient/check_files_test.go | Updates expected output after list-macro counting changes. |
| packaging/snclient.ini | Enables CheckMulti by default and documents max checks. |
| Makefile | Includes check_multi in doc generation command list. |
| docs/checks/commands/check_multi.md | Adds user-facing documentation for check_multi. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Suppressed comments (9)
pkg/snclient/check_multi.go:186
Config.Sectioncreates and registers a section when it does not exist. Becauseconfigis check input, repeated probes of unique missing names permanently growconfig.sectionsand may later serialize those empty sections. Look up the section without the creating accessor.
sec := snc.config.Section(secName)
if len(sec.keys) == 0 {
pkg/snclient/check_multi.go:269
- Preserving each child state in
_statecausesCheckData.setStateFromMapsto unconditionally promote any nonzerocrit_count/warn_count, after the user-defined aggregate conditions run. Consequently, a critical child still makes the result CRITICAL withcritical=none, so custom conditions cannot lower or remap child severities as advertised. Add a check_multi-specific way to disable this implicit list-state promotion and let its aggregate thresholds determine the result.
"_state": fmt.Sprintf("%d", res.State),
pkg/snclient/check_multi.go:278
- The shallow copy retains each built-in child's
WarningandCriticalcondition lists. Parentcheck.Finalize()callsCheckMetrics, re-evaluates those child thresholds, and can escalate the aggregate result independently of check_multi's configured conditions. This also makescritical=noneineffective for metric-based child failures even after list-state promotion is addressed. Preserve the perfdata thresholds but skip metric threshold evaluation for the aggregate check.
for _, m := range res.Metrics {
metricCopy := *m
metricCopy.Name = fmt.Sprintf("%s::%s", tag, m.Name)
allMetrics = append(allMetrics, &metricCopy)
pkg/snclient/check_multi.go:260
- Only the child's primary
Outputis aggregated, so checks that return additional diagnostic lines inCheckResult.Detailssilently lose them. UseBuildOutputString()to include the complete child output.
detailsList = append(detailsList, fmt.Sprintf("[% 2d] %s %s", idx+1, tag, res.Output))
pkg/snclient/checkdata.go:119
- Adding
unknownThresholdmakesunknown=available to every check, but the existing threshold-normalization paths were not extended:transformKeywordsUsingAttributes,disableGenerallizedConditionsUsingAttributes, and the drive-specific blacklist/keyword transforms still process only warning, critical, and OK lists. Unknown conditions using aliases or drive-specialized keywords therefore behave differently and may not match the same entries. IncludeunknownThresholdin those normalization paths or scope the argument to checks that support it.
unknownThreshold ConditionList
defaultUnknown string
pkg/snclient/config.go:352
- This detects only a literal space. A valid command separated from its first
key=valueargument by a tab (for example,check_process\tprocess=123) falls through as a named INI entry and later tries to execute123instead ofcheck_process. Treat tabs as command whitespace too.
if isMultiSection && strings.Contains(val[0], " ") {
pkg/snclient/config.go:340
SetRawinterprets any key ending in+as INI append syntax and strips the suffix. Here the entire line is a command, so a bare executable/argument ending in+is silently changed before execution. Store raw command keys without applying key-append semantics.
if err := currentSection.SetRaw(line, ""); err != nil {
parseErrors = append(parseErrors, fmt.Errorf("config error in %s:%d: %s", iniPath, lineNr, err.Error()))
}
pkg/snclient/config.go:355
- This raw-command path also passes the whole command through
SetRaw, which strips a trailing+as append syntax. For example,check_dummy message=+is persisted and executed ascheck_dummy message=. Store the command key without applying INI key operators.
if err := currentSection.SetRaw(line, ""); err != nil {
parseErrors = append(parseErrors, fmt.Errorf("config error in %s:%d: %s", iniPath, lineNr, err.Error()))
}
docs/checks/commands/check_multi.md:105
- This states that the attributes can be used in filters, but
check_multiexplicitly rejects bothfilterandfilter+(disableFilter: true). Update the help/documentation generator for filter-disabled checks so this section describes threshold keywords only; otherwise users are directed to an option that always returns UNKNOWN.
### Filter Keywords
these can be used in filters and thresholds (along with the default attributes):
| if isKnown { | ||
| return snc.RunCheckWithContext(ctx, cmdName, cmdArgs, 0, nil, false), false |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (6)
pkg/snclient/check_multi.go:183
max checkslimits only this invocation's immediate children. Because child commands can themselves becheck_multi, a valid config can fan out to 20^6 executions before the depth guard, and fast handlers such ascheck_dummyignore an expired context. Track a shared remaining budget in the context and decrement it for every child so nested configurations cannot bypass the configured execution limit.
if int64(len(childChecks)) > maxChecks {
return &CheckResult{
State: CheckExitUnknown,
Output: fmt.Sprintf("number of checks (%d) exceeds max checks limit (%d)", len(childChecks), maxChecks),
}, nil
pkg/snclient/config.go:801
- This serialization branch emits a bare key, but
ParseINIrejects every non-comment line without=(lines 327-332). An empty value in a multi section therefore cannot survive a save/reload round trip. Keep the normalkey =representation unless matching parser support for bare commands is added.
if strings.HasPrefix(cs.name, "/settings/check/multi/") {
data = append(data, key)
} else {
data = append(data, fmt.Sprintf("%s =", key))
pkg/snclient/check_multi.go:128
- A missing
CheckMultikey is treated as disabled becauseGetBoolreturnsfalsewhen absent, whileDefaultConfig["/modules"]does not define this module. Minimal/custom configurations therefore contradict the documented “enabled by default” behavior (the new tests all explicitly enable it). Add the module to the programmatic defaults, not only the packaged INI.
enabled, _, _ := snc.config.Section("/modules").GetBool("CheckMulti")
if !enabled {
pkg/snclient/check_multi.go:309
CheckResult.OutputexcludesDetails; the publicBuildOutputStringmethod is what combines them. Consequently, nested checks that produce detail output silently lose those lines here, and theoutputattribute also sees incomplete output. Build one combined child output and use it for both aggregation and the entry attribute.
firstLine := strings.TrimSpace(strings.Split(res.Output, "\n")[0])
detailsList = append(detailsList, fmt.Sprintf("[%s] %s", tag, res.Output))
entryState := fmt.Sprintf("%d", res.State)
entry := map[string]string{
"name": tag,
"tag": tag,
"command": chk.cmdStr,
"state": entryState,
"status": res.StateString(),
"shortoutput": firstLine,
"output": res.Output,
pkg/snclient/check_multi.go:336
- These shallow copies retain each child metric's
WarningandCriticalcondition lists. Parentcheck.Finalize()then callsCheckMetricson them, so a child metric can re-escalate the aggregate even when check_multi's custom count/entry thresholds intentionally classify that child as OK; metric-free children behave differently. Preserve the child thresholds for perfdata rendering, but prevent inherited child conditions from participating in the parent's state calculation.
for _, m := range res.Metrics {
metricCopy := *m
metricCopy.Name = fmt.Sprintf("%s::%s", tag, m.Name)
allMetrics = append(allMetrics, &metricCopy)
pkg/snclient/check_multi.go:81
- The PR description advertises inline
check=...arguments and untagged config entries, but this API only registerscommandas a tagged list and requirescommand[tag]=...; the advertised invocations return UNKNOWN. Either retain compatibility with the described syntax or update the PR contract and examples to make this breaking syntax choice explicit.
"command": {value: &l.commands, description: "Check command to execute with mandatory unique tag, e.g. command[tag]=..."},
"config": {value: &l.config, description: "Config section name under [/settings/check/multi/< section >] to execute"},
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
pkg/snclient/checkdata.go:1125
- Bracket suffixes are normalized before checking the destination type, so this makes tagged spellings valid for every existing check argument, not just
command. For example,path[x]=...is now accepted aspath, and differently tagged list arguments can repeatedly clear one another becausehasArgsSupplieduses the full keyword. Reject bracket syntax unless the resolved argument is aTaggedCommandList.
if before, rest, found := strings.Cut(keyword, "["); found && strings.HasSuffix(keyword, "]") {
lookupKey = before
tag = rest[:len(rest)-1]
pkg/snclient/check_multi.go:312
- Child output is copied into
CheckResult.Details, whichCheckData.finalizeOutputlater processes withCheckResult.Finalize. As a result, literal child text such as%(count),%(status), or{{ IF ... }}is interpreted using the parent's macros and can be rewritten or removed. Aggregate plugin output as literal text rather than passing untrusted child output back through the template engine.
firstLine := strings.TrimSpace(strings.Split(childOutput, "\n")[0])
detailsList = append(detailsList, fmt.Sprintf("[%s] %s", tag, childOutput))
pkg/snclient/check_multi.go:259
- Config-mode tags are not trimmed, unlike inline tags in
parseTaggedCommand. Consequentlycommand[foo]andcommand[ foo ]are treated as distinct tags, bypassing the uniqueness check and producing whitespace-dependent detail/performance labels. Normalize the extracted config tag before validation and duplicate detection.
tag := strings.TrimSuffix(strings.TrimPrefix(key, "command["), "]")
| secName := "/settings/check/multi/" + l.config | ||
| sec := snc.config.Section(secName) | ||
|
|
||
| if len(sec.keys) == 0 { |
Uh oh!
There was an error while loading. Please reload this page.