@@ -20,6 +20,13 @@ type QueryConfig struct {
2020 LimitEntries int // Limit output entries (0 = no limit)
2121 TailLines int // Number of lines to show from end (for tail operation)
2222 SeekToRow int64 // Row number to seek to (0-based)
23+ // Search operation parameters
24+ SearchPattern string // Regex pattern to search for
25+ AfterContext int // Lines to show after match
26+ BeforeContext int // Lines to show before match
27+ Context int // Lines to show before and after match
28+ CaseSensitive bool // Case-sensitive search
29+ InvertMatch bool // Show non-matching lines
2330 // Buildkite API parameters
2431 Organization string
2532 Pipeline string
@@ -78,6 +85,11 @@ func runStreamingQuery(reader *buildkitelogs.ParquetReader, config *QueryConfig)
7885 return fmt .Errorf ("group pattern is required for by-group operation" )
7986 }
8087 return streamByGroup (reader , config , start )
88+ case "search" :
89+ if config .SearchPattern == "" {
90+ return fmt .Errorf ("pattern is required for search operation" )
91+ }
92+ return streamSearch (reader , config , start )
8193 case "info" :
8294 return showFileInfo (reader , config )
8395 case "tail" :
@@ -188,6 +200,40 @@ func streamListCommands(reader *buildkitelogs.ParquetReader, config *QueryConfig
188200 return formatStreamingCommandsResult (commands , totalEntries , commandCount , queryTime , config )
189201}
190202
203+ // streamSearch handles search operation using streaming with regex pattern matching and context lines
204+ func streamSearch (reader * buildkitelogs.ParquetReader , config * QueryConfig , start time.Time ) error {
205+ // Create search options
206+ options := buildkitelogs.SearchOptions {
207+ Pattern : config .SearchPattern ,
208+ CaseSensitive : config .CaseSensitive ,
209+ InvertMatch : config .InvertMatch ,
210+ BeforeContext : config .BeforeContext ,
211+ AfterContext : config .AfterContext ,
212+ Context : config .Context ,
213+ }
214+
215+ var results []buildkitelogs.SearchResult
216+ matchesFound := 0
217+
218+ for result , err := range reader .SearchEntriesIter (options ) {
219+ if err != nil {
220+ return fmt .Errorf ("error during search: %w" , err )
221+ }
222+
223+ matchesFound ++
224+ results = append (results , result )
225+
226+ // Apply limit if specified
227+ if config .LimitEntries > 0 && matchesFound >= config .LimitEntries {
228+ break
229+ }
230+ }
231+
232+ // Format output
233+ queryTime := float64 (time .Since (start ).Nanoseconds ()) / 1e6
234+ return formatSearchResultsLibrary (results , matchesFound , queryTime , config )
235+ }
236+
191237// streamByGroup handles by-group operation using streaming with optional limiting
192238func streamByGroup (reader * buildkitelogs.ParquetReader , config * QueryConfig , start time.Time ) error {
193239 var entries []buildkitelogs.ParquetLogEntry
@@ -341,6 +387,99 @@ func formatStreamingCommandsResult(commands []buildkitelogs.ParquetLogEntry, tot
341387 return nil
342388}
343389
390+ // formatSearchResultsLibrary formats search results with context lines using library types
391+ func formatSearchResultsLibrary (results []buildkitelogs.SearchResult , matchesFound int , queryTime float64 , config * QueryConfig ) error {
392+ if config .Format == "json" {
393+ result := struct {
394+ Matches []buildkitelogs.SearchResult `json:"matches"`
395+ Stats struct {
396+ MatchesFound int `json:"matches_found"`
397+ QueryTime float64 `json:"query_time_ms"`
398+ } `json:"stats,omitempty"`
399+ }{
400+ Matches : results ,
401+ }
402+
403+ if config .ShowStats {
404+ result .Stats .MatchesFound = matchesFound
405+ result .Stats .QueryTime = queryTime
406+ }
407+
408+ encoder := json .NewEncoder (os .Stdout )
409+ encoder .SetIndent ("" , " " )
410+ return encoder .Encode (result )
411+ }
412+
413+ // Text format
414+ limitText := ""
415+ if config .LimitEntries > 0 && matchesFound >= config .LimitEntries {
416+ limitText = fmt .Sprintf (" (limited to %d)" , config .LimitEntries )
417+ }
418+ fmt .Fprintf (os .Stderr , "Matches found: %d%s\n \n " , matchesFound , limitText )
419+
420+ if len (results ) == 0 {
421+ fmt .Fprintln (os .Stderr , "No matches found." )
422+ return nil
423+ }
424+
425+ for i , result := range results {
426+ if i > 0 {
427+ fmt .Println ("--" )
428+ }
429+
430+ // Print before context
431+ for _ , entry := range result .BeforeContext {
432+ timestamp := time .Unix (0 , entry .Timestamp * int64 (time .Millisecond ))
433+ if entry .Group != "" {
434+ fmt .Printf ("[%s] [%s] %s\n " ,
435+ timestamp .Format ("2006-01-02 15:04:05.000" ),
436+ entry .Group ,
437+ entry .Content )
438+ } else {
439+ fmt .Printf ("[%s] %s\n " ,
440+ timestamp .Format ("2006-01-02 15:04:05.000" ),
441+ entry .Content )
442+ }
443+ }
444+
445+ // Print match line (highlighted)
446+ timestamp := time .Unix (0 , result .Match .Timestamp * int64 (time .Millisecond ))
447+ if result .Match .Group != "" {
448+ fmt .Printf ("[%s] [%s] MATCH: %s\n " ,
449+ timestamp .Format ("2006-01-02 15:04:05.000" ),
450+ result .Match .Group ,
451+ result .Match .Content )
452+ } else {
453+ fmt .Printf ("[%s] MATCH: %s\n " ,
454+ timestamp .Format ("2006-01-02 15:04:05.000" ),
455+ result .Match .Content )
456+ }
457+
458+ // Print after context
459+ for _ , entry := range result .AfterContext {
460+ timestamp := time .Unix (0 , entry .Timestamp * int64 (time .Millisecond ))
461+ if entry .Group != "" {
462+ fmt .Printf ("[%s] [%s] %s\n " ,
463+ timestamp .Format ("2006-01-02 15:04:05.000" ),
464+ entry .Group ,
465+ entry .Content )
466+ } else {
467+ fmt .Printf ("[%s] %s\n " ,
468+ timestamp .Format ("2006-01-02 15:04:05.000" ),
469+ entry .Content )
470+ }
471+ }
472+ }
473+
474+ if config .ShowStats {
475+ fmt .Fprintf (os .Stderr , "\n --- Search Statistics (Streaming) ---\n " )
476+ fmt .Fprintf (os .Stderr , "Matches found: %d\n " , matchesFound )
477+ fmt .Fprintf (os .Stderr , "Query time: %.2f ms\n " , queryTime )
478+ }
479+
480+ return nil
481+ }
482+
344483// formatStreamingEntriesResult formats entries output from streaming query
345484func formatStreamingEntriesResult (entries []buildkitelogs.ParquetLogEntry , totalEntries , matchedEntries int , queryTime float64 , config * QueryConfig ) error {
346485 if config .Format == "json" {
0 commit comments