diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-02 21:23:00 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-02 21:23:00 +0300 |
| commit | 17ee5e62c2b1037c21cb36f2677d2c538e2542cb (patch) | |
| tree | 8fbed16de9d7ae13307216551cbdafcd34127bf9 | |
| parent | e7a64c7e338e0d8818425e67650e673240d9b853 (diff) | |
feat: add server info message for literal grep mode
- Add IsLiteral() and Pattern() methods to regex.Regex struct
- Log info message when grep uses optimized literal string matching
- Fix bug where grep commands were processed as cat commands
- Add comprehensive integration tests to verify literal mode messages
This gives users visibility when the performance-optimized literal
string matching is being used instead of regex matching.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | integrationtests/dgrep_literal_info_test.go | 191 | ||||
| -rw-r--r-- | internal/regex/regex.go | 10 | ||||
| -rw-r--r-- | internal/server/handlers/readcommand.go | 28 | ||||
| -rw-r--r-- | internal/server/handlers/serverhandler.go | 8 |
4 files changed, 236 insertions, 1 deletions
diff --git a/integrationtests/dgrep_literal_info_test.go b/integrationtests/dgrep_literal_info_test.go new file mode 100644 index 0000000..c007263 --- /dev/null +++ b/integrationtests/dgrep_literal_info_test.go @@ -0,0 +1,191 @@ +package integrationtests + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/mimecast/dtail/internal/config" +) + +// TestDGrepLiteralModeInfo verifies that the server logs info message when using literal mode +func TestDGrepLiteralModeInfo(t *testing.T) { + if !config.Env("DTAIL_INTEGRATION_TEST_RUN_MODE") { + t.Log("Skipping") + return + } + + cleanupTmpFiles(t) + testLogger := NewTestLogger("TestDGrepLiteralModeInfo") + defer testLogger.WriteLogFile() + + // Create test data + testData := `ERROR test line 1 +WARNING test line 2 +ERROR test line 3 +INFO test line 4 +ERROR test line 5 +` + testFile := "literal_info_test.log.tmp" + if err := os.WriteFile(testFile, []byte(testData), 0644); err != nil { + t.Fatal("Failed to create test file:", err) + } + defer os.Remove(testFile) + + // Test patterns - both literal and regex + tests := []struct { + name string + pattern string + expectLiteral bool + expectedCount int + }{ + { + name: "SimpleLiteral", + pattern: "ERROR", + expectLiteral: true, + expectedCount: 3, + }, + { + name: "LiteralWithSpace", + pattern: "ERROR test", + expectLiteral: true, + expectedCount: 3, + }, + { + name: "RegexPattern", + pattern: "ERROR.*line [0-9]", + expectLiteral: false, + expectedCount: 3, + }, + { + name: "RegexAlternation", + pattern: "(ERROR|WARNING)", + expectLiteral: false, + expectedCount: 4, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + port := getUniquePortNumber() + bindAddress := "localhost" + + ctx, cancel := context.WithCancel(context.Background()) + ctx = WithTestLogger(ctx, testLogger) + defer cancel() + + // Start dserver with info log level to capture our message + stdoutCh, stderrCh, _, err := startCommand(ctx, t, + "", "../dserver", + "--cfg", "none", + "--logger", "stdout", + "--logLevel", "info", // Changed from error to info + "--bindAddress", bindAddress, + "--port", fmt.Sprintf("%d", port), + ) + if err != nil { + t.Error(err) + return + } + + // Capture server output + var serverOutput strings.Builder + var outputMutex sync.Mutex + outputDone := make(chan struct{}) + + go func() { + defer close(outputDone) + for { + select { + case line := <-stdoutCh: + outputMutex.Lock() + serverOutput.WriteString(line) + serverOutput.WriteString("\n") + outputMutex.Unlock() + case line := <-stderrCh: + outputMutex.Lock() + serverOutput.WriteString(line) + serverOutput.WriteString("\n") + outputMutex.Unlock() + case <-ctx.Done(): + return + } + } + }() + + // Give server time to start + time.Sleep(500 * time.Millisecond) + + // Run dgrep + outFile := fmt.Sprintf("dgrep_info_%s.stdout.tmp", test.name) + defer os.Remove(outFile) + + _, err = runCommand(ctx, t, outFile, + "../dgrep", + "--plain", + "--cfg", "none", + "--grep", test.pattern, + "--servers", fmt.Sprintf("%s:%d", bindAddress, port), + "--trustAllHosts", + "--noColor", + "--files", testFile) + + if err != nil { + t.Errorf("Failed to run dgrep with pattern '%s': %v", test.pattern, err) + return + } + + // Give time for server output to be captured + time.Sleep(500 * time.Millisecond) + + // Stop server + cancel() + + // Wait for output capture goroutine to finish + select { + case <-outputDone: + case <-time.After(2 * time.Second): + t.Log("Warning: output capture goroutine did not finish in time") + } + + // Check grep output for correctness + content, err := os.ReadFile(outFile) + if err != nil { + t.Errorf("Failed to read output file: %v", err) + return + } + + lines := strings.Split(strings.TrimSpace(string(content)), "\n") + actualCount := 0 + for _, line := range lines { + if line != "" { + actualCount++ + } + } + + if actualCount != test.expectedCount { + t.Errorf("Pattern '%s': expected %d matches, got %d", test.pattern, test.expectedCount, actualCount) + } + + // Check server output for literal mode message + outputMutex.Lock() + serverLog := serverOutput.String() + outputMutex.Unlock() + // The server uses structured logging, so the message appears as "pattern:|<pattern>" + literalMsg := fmt.Sprintf("Using optimized literal string matching for pattern:|%s", test.pattern) + containsLiteralMsg := strings.Contains(serverLog, literalMsg) + + if test.expectLiteral && !containsLiteralMsg { + t.Errorf("Expected literal mode info message for pattern '%s' but didn't find it", test.pattern) + t.Logf("Server output:\n%s", serverLog) + } else if !test.expectLiteral && containsLiteralMsg { + t.Errorf("Did not expect literal mode info message for pattern '%s' but found it", test.pattern) + t.Logf("Server output:\n%s", serverLog) + } + }) + } +}
\ No newline at end of file diff --git a/internal/regex/regex.go b/internal/regex/regex.go index eb4e86e..b817bc4 100644 --- a/internal/regex/regex.go +++ b/internal/regex/regex.go @@ -169,6 +169,16 @@ func (r Regex) Serialize() (string, error) { return fmt.Sprintf("regex:%s %s", strings.Join(flags, ","), r.regexStr), nil } +// IsLiteral returns true if this regex is using literal string matching +func (r Regex) IsLiteral() bool { + return r.isLiteral +} + +// Pattern returns the original pattern string +func (r Regex) Pattern() string { + return r.regexStr +} + // Deserialize the regex. func Deserialize(str string) (Regex, error) { // Get regex string diff --git a/internal/server/handlers/readcommand.go b/internal/server/handlers/readcommand.go index 8a9d0aa..1f46498 100644 --- a/internal/server/handlers/readcommand.go +++ b/internal/server/handlers/readcommand.go @@ -198,6 +198,16 @@ func (r *readCommand) read(ctx context.Context, ltx lcontext.LContext, path, globID string, re regex.Regex) { dlog.Server.Info(r.server.user, "Start reading", path, globID) + + // Log if grep is using literal mode optimization + if r.mode == omode.GrepClient { + if re.IsLiteral() { + dlog.Server.Info(r.server.user, "Using optimized literal string matching for pattern:", re.Pattern()) + } else { + dlog.Server.Info(r.server.user, "Using regex matching for pattern:", re.Pattern()) + } + } + var reader fs.FileReader var limiter chan struct{} @@ -284,6 +294,15 @@ func (r *readCommand) readWithProcessor(ctx context.Context, ltx lcontext.LConte path, globID string, re regex.Regex, reader fs.FileReader) { dlog.Server.Info(r.server.user, "Using channel-less grep implementation", path, globID) + + // Log if grep is using literal mode optimization + if r.mode == omode.GrepClient { + if re.IsLiteral() { + dlog.Server.Info(r.server.user, "Using optimized literal string matching for pattern:", re.Pattern()) + } else { + dlog.Server.Info(r.server.user, "Using regex matching for pattern:", re.Pattern()) + } + } // Use the existing lines channel but with the processor-based reader lines := r.server.lines @@ -337,6 +356,15 @@ func (r *readCommand) readWithTurboProcessor(ctx context.Context, ltx lcontext.L path, globID string, re regex.Regex, reader fs.FileReader) { dlog.Server.Info(r.server.user, "Using turbo channel-less implementation", path, globID) + + // Log if grep is using literal mode optimization + if r.mode == omode.GrepClient { + if re.IsLiteral() { + dlog.Server.Info(r.server.user, "Using optimized literal string matching for pattern:", re.Pattern()) + } else { + dlog.Server.Info(r.server.user, "Using regex matching for pattern:", re.Pattern()) + } + } // Enable turbo mode if not already enabled if !r.server.IsTurboMode() { diff --git a/internal/server/handlers/serverhandler.go b/internal/server/handlers/serverhandler.go index 9163447..da27066 100644 --- a/internal/server/handlers/serverhandler.go +++ b/internal/server/handlers/serverhandler.go @@ -74,7 +74,13 @@ func (h *ServerHandler) handleUserCommand(ctx context.Context, ltx lcontext.LCon } switch commandName { - case "grep", "cat": + case "grep": + command := newReadCommand(h, omode.GrepClient) + go func() { + command.Start(ctx, ltx, argc, args, 1) + commandFinished() + }() + case "cat": command := newReadCommand(h, omode.CatClient) go func() { command.Start(ctx, ltx, argc, args, 1) |
