From 6adccab7d44a8e5f3c94f2cf36d4ac250642a13a Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Mon, 15 Jun 2026 11:06:03 +0200 Subject: [PATCH 1/3] check_logfile: imporve file scanning and keep track of lines matched in each file apply conditional filters to the lines early on while adding them, this was not the case before. now the count of lines being passing the filter is correct. add function to generate a detail string from files and the count of matched lines in the file. this can be added to the detailed syntax when needed. if there are no lines matched across all files, change the main output line to say "No matching lines found in files" --- pkg/snclient/check_logfile.go | 125 +++++++++++++++++++++-------- pkg/snclient/check_logfile_test.go | 10 +++ 2 files changed, 103 insertions(+), 32 deletions(-) diff --git a/pkg/snclient/check_logfile.go b/pkg/snclient/check_logfile.go index 376278f4..451e86b3 100644 --- a/pkg/snclient/check_logfile.go +++ b/pkg/snclient/check_logfile.go @@ -22,14 +22,14 @@ func init() { var numReg = regexp.MustCompile(`\d+`) type CheckLogFile struct { - snc *Agent - FilePath []string - Paths string - LineDelimiter string - TimestampPattern string - ColumnDelimiter string - LabelPattern []string - Offset string // Changed to string to detect if user provided it + snc *Agent + FilePathPatterns []string + FilePathPatternsCS string + LineDelimiter string + TimestampPattern string + ColumnDelimiter string + LabelPattern []string + Offset string // Changed to string to detect if user provided it } type LogLine struct { @@ -69,8 +69,8 @@ func (c *CheckLogFile) Build() *CheckData { emptySyntax: "%(status) - No files found", emptyState: CheckExitUnknown, args: map[string]CheckArgument{ - "file": {value: &c.FilePath, description: "The file that should be checked"}, - "files": {value: &c.Paths, description: "Comma separated list of files"}, + "file": {value: &c.FilePathPatterns, description: "The file that should be checked"}, + "files": {value: &c.FilePathPatternsCS, description: "Comma separated list of files"}, "offset": {value: &c.Offset, description: "Starting position (in bytes) for scanning the file (0 for beginning). This overrides any saved offset"}, "line-split": {value: &c.LineDelimiter, description: "Character string used to split a file into several lines (default \\n)"}, "column-split": {value: &c.ColumnDelimiter, description: "Tab split default: \\t"}, @@ -104,8 +104,8 @@ func (c *CheckLogFile) Check(_ context.Context, snc *Agent, check *CheckData, _ return nil, fmt.Errorf("module CheckLogFile is not enabled in /modules section") } - c.FilePath = append(c.FilePath, strings.Split(c.Paths, ",")...) - if len(c.FilePath) == 0 { + c.FilePathPatterns = append(c.FilePathPatterns, strings.Split(c.FilePathPatternsCS, ",")...) + if len(c.FilePathPatterns) == 0 { return nil, fmt.Errorf("no file defined") } @@ -122,47 +122,104 @@ func (c *CheckLogFile) Check(_ context.Context, snc *Agent, check *CheckData, _ } } + // patterns are for the file names/paths, not file contents! allowedPattern := c.getAllowedPattern() - totalLineCount := 0 - for _, fileName := range c.FilePath { + totalLineIndexedCount := 0 + checkedFilesWithMatchedEntries := make(map[string]int, 0) + + for _, fileName := range c.FilePathPatterns { if fileName == "" { continue } - count := 0 + + lineIndexedInThisFilePattern := 0 files, err := filepath.Glob(fileName) if err != nil { return nil, fmt.Errorf("could not get files for pattern %s, error was: %s", fileName, err.Error()) } + for _, fileName := range files { if !c.matchPattern(fileName, allowedPattern) { + log.Tracef("check_logfile rejecting file: %s as it does not any match patterns: %v ", fileName, allowedPattern) + return nil, fmt.Errorf("file %s does not match any allowed pattern", fileName) } - tmpCount, err := c.addFile(fileName, check, patterns) + + log.Debugf("check_logfile adding file: %s", fileName) + entries, lineIndex, err := c.addFile(fileName, check, patterns) if err != nil { return nil, fmt.Errorf("error for file %s, error was: %s", fileName, err.Error()) } - count += tmpCount + log.Debugf("check_logfile file: %s | returned entries: %v | lines indexed: %d", fileName, entries, lineIndex) + + lineIndexedInThisFilePattern += lineIndex + check.listData = append(check.listData, entries...) + checkedFilesWithMatchedEntries[fileName] = len(entries) } - totalLineCount += count + + totalLineIndexedCount += lineIndexedInThisFilePattern } + check.details = map[string]string{ - "total": fmt.Sprintf("%d", totalLineCount), + "total": fmt.Sprintf("%d", totalLineIndexedCount), + "file_counts": c.buildFileCountsDetailString(checkedFilesWithMatchedEntries), + } + + if len(check.listData) == 0 { + check.emptySyntax = fmt.Sprintf("%%(status) - No matching lines found in files (%s)", check.details["file_counts"]) } return check.Finalize() } -func (c *CheckLogFile) addFile(fileName string, check *CheckData, labels map[string]*regexp.Regexp) (int, error) { +func (c *CheckLogFile) buildFileCountsDetailString(checkedFilesWithMatchedEntries map[string]int) (fileCountDetails string) { + type kv struct { + file string + count int + } + sorted := make([]kv, 0, len(checkedFilesWithMatchedEntries)) + for file, count := range checkedFilesWithMatchedEntries { + sorted = append(sorted, kv{file, count}) + } + + slices.SortFunc(sorted, func(a, b kv) int { + if a.file < b.file { + return -1 + } + if a.file > b.file { + return 1 + } + if a.count < b.count { + return -1 + } + if a.count > b.count { + return 1 + } + + return 0 + }) + + detailParts := make([]string, 0, len(sorted)) + for _, item := range sorted { + detailParts = append(detailParts, fmt.Sprintf("%s: %d", item.file, item.count)) + } + + fileCountDetails = strings.Join(detailParts, ", ") + + return fileCountDetails +} + +func (c *CheckLogFile) addFile(fileName string, check *CheckData, labels map[string]*regexp.Regexp) (entries []map[string]string, lineIndex int, err error) { file, err := os.Open(fileName) if err != nil { - return 0, fmt.Errorf("could not open file: %s error was: %s", fileName, err.Error()) + return entries, 0, fmt.Errorf("could not open file: %s error was: %s", fileName, err.Error()) } defer file.Close() info, err := file.Stat() if err != nil { - return 0, fmt.Errorf("could not stat file %s: %s", fileName, err.Error()) + return entries, 0, fmt.Errorf("could not stat file %s: %s", fileName, err.Error()) } currentInode := getInode(fileName) @@ -170,7 +227,7 @@ func (c *CheckLogFile) addFile(fileName string, check *CheckData, labels map[str startOffset, err := c.getStartOffset(fileName, currentSize, currentInode) if err != nil { - return 0, err + return entries, 0, err } saveState := true @@ -188,25 +245,23 @@ func (c *CheckLogFile) addFile(fileName string, check *CheckData, labels map[str // seek to start offset if startOffset > 0 { if startOffset > currentSize { - return 0, nil + return entries, 0, nil } _, err = file.Seek(startOffset, 0) if err != nil { saveState = false - return 0, fmt.Errorf("failed to seek to offset %d in %s: %w", startOffset, fileName, err) + return entries, 0, fmt.Errorf("failed to seek to offset %d in %s: %w", startOffset, fileName, err) } } scanner := bufio.NewScanner(file) scanner.Split(c.getCustomSplitFunction()) - okReset := len(check.okThreshold) > 0 + okThresholdNotEmpty := len(check.okThreshold) > 0 lineStorage := make([]map[string]string, 0) columnNumbers := c.getRequiredColumnNumbers(check) - // filter each line - var lineIndex int for lineIndex = 0; scanner.Scan(); lineIndex++ { line := scanner.Text() entry := map[string]string{ @@ -230,9 +285,16 @@ func (c *CheckLogFile) addFile(fileName string, check *CheckData, labels map[str } } + if !check.MatchMapCondition(check.filter, entry, false) { + log.Tracef("file: %s , line : %s, did not match the filter set in the check, not ading to check.listData", fileName, line) + + continue + } + lineStorage = append(lineStorage, entry) - // Do not check for OK with empty condition list, it would match all - if okReset && check.MatchMapCondition(check.okThreshold, entry, true) { + + // Do not check for OK condition if the OK condition list is empty, it would match everything + if okThresholdNotEmpty && check.MatchMapCondition(check.okThreshold, entry, true) { // add and empty entry with the current line count to the list data to keep track of line count entry := map[string]string{ "_count": fmt.Sprintf("%d", len(lineStorage)), @@ -241,9 +303,8 @@ func (c *CheckLogFile) addFile(fileName string, check *CheckData, labels map[str lineStorage = make([]map[string]string, 0) } } - check.listData = append(check.listData, lineStorage...) - return lineIndex, nil + return lineStorage, lineIndex, nil } func (c *CheckLogFile) getStartOffset(fileName string, currentSize int64, currentInode uint64) (int64, error) { diff --git a/pkg/snclient/check_logfile_test.go b/pkg/snclient/check_logfile_test.go index 4811ad54..8b90dd93 100644 --- a/pkg/snclient/check_logfile_test.go +++ b/pkg/snclient/check_logfile_test.go @@ -116,3 +116,13 @@ func TestCheckLogFileColumnN(t *testing.T) { StopTestAgent(t, snc) } + +func TestCheckLogFileFileExistsButHasNoLines(t *testing.T) { + snc := StartTestAgent(t, testLogfileConfig) + + res := snc.RunCheck("check_logfile", []string{"files=./t/test*", "filter=line LIKE this-pattern-does-not-exist-in-the-test-files", "show-all"}) + assert.Equalf(t, CheckExitUnknown, res.State, "state UNKNOWN") + assert.Contains(t, string(res.BuildPluginOutput()), "UNKNOWN - No matching lines found in files ") + + StopTestAgent(t, snc) +} From e1d777ce234f2301bf05ef0279a9361e993a12c1 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Tue, 16 Jun 2026 17:45:07 +0200 Subject: [PATCH 2/3] change the default empty state to ok differentiate between empty states 1) where no files are found, due to given file paths 2) where files are found but do not contain matched lines. write different outputs for both of them, and add tests --- docs/checks/commands/check_logfile.md | 2 +- pkg/snclient/check_logfile.go | 7 +++++-- pkg/snclient/check_logfile_test.go | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/checks/commands/check_logfile.md b/docs/checks/commands/check_logfile.md index a0db1f3e..0f4d0467 100644 --- a/docs/checks/commands/check_logfile.md +++ b/docs/checks/commands/check_logfile.md @@ -58,7 +58,7 @@ Naemon Config | Argument | Default Value | | ------------- | ---------------------------------------------------------------------- | -| empty-state | 3 (UNKNOWN) | +| empty-state | 0 (OK) | | empty-syntax | %(status) - No files found | | top-syntax | %(status) - %(problem_count)/%(count) lines (%(count)) %(problem_list) | | ok-syntax | %(status) - All %(count) / %(total) Lines OK | diff --git a/pkg/snclient/check_logfile.go b/pkg/snclient/check_logfile.go index 451e86b3..4d78e8e3 100644 --- a/pkg/snclient/check_logfile.go +++ b/pkg/snclient/check_logfile.go @@ -67,7 +67,7 @@ func (c *CheckLogFile) Build() *CheckData { okSyntax: "%(status) - All %(count) / %(total) Lines OK", topSyntax: "%(status) - %(problem_count)/%(count) lines (%(count)) %(problem_list)", emptySyntax: "%(status) - No files found", - emptyState: CheckExitUnknown, + emptyState: CheckExitOK, args: map[string]CheckArgument{ "file": {value: &c.FilePathPatterns, description: "The file that should be checked"}, "files": {value: &c.FilePathPatternsCS, description: "Comma separated list of files"}, @@ -166,8 +166,11 @@ func (c *CheckLogFile) Check(_ context.Context, snc *Agent, check *CheckData, _ "file_counts": c.buildFileCountsDetailString(checkedFilesWithMatchedEntries), } - if len(check.listData) == 0 { + if len(checkedFilesWithMatchedEntries) == 0 { + check.okSyntax = "%(status) - No files found to search lines in" + } else if len(check.listData) == 0 { check.emptySyntax = fmt.Sprintf("%%(status) - No matching lines found in files (%s)", check.details["file_counts"]) + check.emptyStateSet = true } return check.Finalize() diff --git a/pkg/snclient/check_logfile_test.go b/pkg/snclient/check_logfile_test.go index 8b90dd93..a98b7856 100644 --- a/pkg/snclient/check_logfile_test.go +++ b/pkg/snclient/check_logfile_test.go @@ -121,8 +121,18 @@ func TestCheckLogFileFileExistsButHasNoLines(t *testing.T) { snc := StartTestAgent(t, testLogfileConfig) res := snc.RunCheck("check_logfile", []string{"files=./t/test*", "filter=line LIKE this-pattern-does-not-exist-in-the-test-files", "show-all"}) - assert.Equalf(t, CheckExitUnknown, res.State, "state UNKNOWN") - assert.Contains(t, string(res.BuildPluginOutput()), "UNKNOWN - No matching lines found in files ") + assert.Equalf(t, CheckExitOK, res.State, "state should be OK") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - No matching lines found in files") + + StopTestAgent(t, snc) +} + +func TestCheckLogFileFileDoesNotExist(t *testing.T) { + snc := StartTestAgent(t, testLogfileConfig) + + res := snc.RunCheck("check_logfile", []string{"files=./t/testfiledoesnotexist*"}) + assert.Equalf(t, CheckExitOK, res.State, "state should be OK") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - No files found to search lines in") StopTestAgent(t, snc) } From 8a14be2f2a4f37c1b33c1ac83d029b099f8cbb9d Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Tue, 23 Jun 2026 14:49:13 +0200 Subject: [PATCH 3/3] check_logfile: empty state is unknown, conditionally set to 0 if files exist without matched lines checkdata.finalizeOutput() requires explicit filters or implicit argument filters to apply emptySyntax on output. apply empty syntax on empty state, by setting the "file" and files" argument is set as a filter. --- docs/checks/commands/check_logfile.md | 2 +- pkg/snclient/check_logfile.go | 11 ++++++----- pkg/snclient/check_logfile_test.go | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/checks/commands/check_logfile.md b/docs/checks/commands/check_logfile.md index 0f4d0467..a0db1f3e 100644 --- a/docs/checks/commands/check_logfile.md +++ b/docs/checks/commands/check_logfile.md @@ -58,7 +58,7 @@ Naemon Config | Argument | Default Value | | ------------- | ---------------------------------------------------------------------- | -| empty-state | 0 (OK) | +| empty-state | 3 (UNKNOWN) | | empty-syntax | %(status) - No files found | | top-syntax | %(status) - %(problem_count)/%(count) lines (%(count)) %(problem_list) | | ok-syntax | %(status) - All %(count) / %(total) Lines OK | diff --git a/pkg/snclient/check_logfile.go b/pkg/snclient/check_logfile.go index 4d78e8e3..f8709a9e 100644 --- a/pkg/snclient/check_logfile.go +++ b/pkg/snclient/check_logfile.go @@ -67,10 +67,10 @@ func (c *CheckLogFile) Build() *CheckData { okSyntax: "%(status) - All %(count) / %(total) Lines OK", topSyntax: "%(status) - %(problem_count)/%(count) lines (%(count)) %(problem_list)", emptySyntax: "%(status) - No files found", - emptyState: CheckExitOK, + emptyState: CheckExitUnknown, args: map[string]CheckArgument{ - "file": {value: &c.FilePathPatterns, description: "The file that should be checked"}, - "files": {value: &c.FilePathPatternsCS, description: "Comma separated list of files"}, + "file": {value: &c.FilePathPatterns, description: "The file that should be checked", isFilter: true}, + "files": {value: &c.FilePathPatternsCS, description: "Comma separated list of files", isFilter: true}, "offset": {value: &c.Offset, description: "Starting position (in bytes) for scanning the file (0 for beginning). This overrides any saved offset"}, "line-split": {value: &c.LineDelimiter, description: "Character string used to split a file into several lines (default \\n)"}, "column-split": {value: &c.ColumnDelimiter, description: "Tab split default: \\t"}, @@ -167,10 +167,11 @@ func (c *CheckLogFile) Check(_ context.Context, snc *Agent, check *CheckData, _ } if len(checkedFilesWithMatchedEntries) == 0 { - check.okSyntax = "%(status) - No files found to search lines in" + check.emptySyntax = fmt.Sprintf("%%(status) - No files found to search lines in, search paths: '%s' ", strings.Join(c.FilePathPatterns, ",")) } else if len(check.listData) == 0 { - check.emptySyntax = fmt.Sprintf("%%(status) - No matching lines found in files (%s)", check.details["file_counts"]) + check.emptyState = CheckExitOK check.emptyStateSet = true + check.emptySyntax = fmt.Sprintf("%%(status) - No matching lines found in files (%s)", check.details["file_counts"]) } return check.Finalize() diff --git a/pkg/snclient/check_logfile_test.go b/pkg/snclient/check_logfile_test.go index a98b7856..b9f4fe43 100644 --- a/pkg/snclient/check_logfile_test.go +++ b/pkg/snclient/check_logfile_test.go @@ -52,7 +52,7 @@ func TestCheckLogFilePathWildCards(t *testing.T) { res = snc.RunCheck("check_logfile", []string{"files=./t/test*", "warn=line LIKE WARNING"}) assert.Equalf(t, CheckExitOK, res.State, "state OK") - assert.Contains(t, string(res.BuildPluginOutput()), "OK - All 0 / 0") + assert.Contains(t, string(res.BuildPluginOutput()), "OK - No matching lines found in files ") StopTestAgent(t, snc) } @@ -131,8 +131,8 @@ func TestCheckLogFileFileDoesNotExist(t *testing.T) { snc := StartTestAgent(t, testLogfileConfig) res := snc.RunCheck("check_logfile", []string{"files=./t/testfiledoesnotexist*"}) - assert.Equalf(t, CheckExitOK, res.State, "state should be OK") - assert.Contains(t, string(res.BuildPluginOutput()), "OK - No files found to search lines in") + assert.Equalf(t, CheckExitUnknown, res.State, "state should be UNKNOWN") + assert.Contains(t, string(res.BuildPluginOutput()), "UNKNOWN - No files found to search lines in") StopTestAgent(t, snc) }