summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-02 21:23:00 +0300
committerPaul Buetow <paul@buetow.org>2025-07-02 21:23:00 +0300
commit17ee5e62c2b1037c21cb36f2677d2c538e2542cb (patch)
tree8fbed16de9d7ae13307216551cbdafcd34127bf9
parente7a64c7e338e0d8818425e67650e673240d9b853 (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.go191
-rw-r--r--internal/regex/regex.go10
-rw-r--r--internal/server/handlers/readcommand.go28
-rw-r--r--internal/server/handlers/serverhandler.go8
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)