Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ logs:
timestamps: false
since: '60m' # set to '' to show all logs
tail: '' # set to 200 to show last 200 lines of logs

### Logs Filtering

When viewing container or service logs in the main panel, you can filter the log output in real-time:

- Press `/` while viewing logs to open the filter prompt
- Type a search string to filter log lines (case-sensitive substring match)
- Press `Enter` to commit the filter and return to viewing filtered logs
- Press `Esc` to cancel the filter and return to unfiltered logs

commandTemplates:
dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates
restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_de.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: zurück
<kbd>/</kbd>: filter logs
</pre>

## Global
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: return
<kbd>/</kbd>: filter logs
</pre>

## Global
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_es.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: regresar
<kbd>/</kbd>: filter logs
</pre>

## Global
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: retour
<kbd>/</kbd>: filter logs
</pre>

## Global
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_nl.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: terug
<kbd>/</kbd>: filter logs
</pre>

## Globaal
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: powrót
<kbd>/</kbd>: filter logs
</pre>

## Globalne
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: retornar
<kbd>/</kbd>: filter logs
</pre>

## Global
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: dönüş
<kbd>/</kbd>: filter logs
</pre>

## Global
Expand Down
1 change: 1 addition & 0 deletions docs/keybindings/Keybindings_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct

<pre>
<kbd>esc</kbd>: 返回
<kbd>/</kbd>: filter logs
</pre>

## 全局
Expand Down
15 changes: 14 additions & 1 deletion pkg/gui/arrangement.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
sidePanelsDirection = boxlayout.ROW
}

showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active || gui.State.LogsFilter.active
infoSectionSize := 0
if showInfoSection {
infoSectionSize = 1
Expand Down Expand Up @@ -112,6 +112,19 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
}...)
}

if gui.State.LogsFilter.active {
return append(result, []*boxlayout.Box{
{
Window: "logsfilterPrefix",
Size: runewidth.StringWidth(gui.logsFilterPrompt()),
},
{
Window: "logsfilter",
Weight: 1,
},
}...)
}

result = append(result,
[]*boxlayout.Box{
{
Expand Down
88 changes: 88 additions & 0 deletions pkg/gui/container_logs.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package gui

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"os/signal"
"strings"
"time"

"github.com/docker/docker/api/types/container"
Expand Down Expand Up @@ -136,12 +139,26 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
}
}

var filterString string
var hasFilter bool

if gui.State.LogsFilter.active {
filterString = gui.State.LogsFilter.needle
hasFilter = filterString != ""
}

if ctr.Details.Config.Tty {
if hasFilter {
return gui.filterTTYLogs(readCloser, writer, filterString, ctx)
}
_, err = io.Copy(writer, readCloser)
if err != nil {
return err
}
} else {
if hasFilter {
return gui.filterNonTTYLogs(readCloser, writer, filterString, ctx)
}
_, err = stdcopy.StdCopy(writer, writer, readCloser)
if err != nil {
return err
Expand All @@ -150,3 +167,74 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,

return nil
}

// filterTTYLogs filters TTY logs line by line based on the filter string
func (gui *Gui) filterTTYLogs(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
return streamFilterLines(reader, writer, filter, ctx)
}

// filterNonTTYLogs filters non-TTY logs (stdout/stderr) line by line
func (gui *Gui) filterNonTTYLogs(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
pipeReader, pipeWriter := io.Pipe()

go func() {
defer pipeWriter.Close()

done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
pipeWriter.CloseWithError(ctx.Err())
case <-done:
}
}()

_, err := stdcopy.StdCopy(pipeWriter, pipeWriter, reader)
close(done)

if err != nil {
pipeWriter.CloseWithError(err)
}
}()

defer pipeReader.Close()
return streamFilterLines(pipeReader, writer, filter, ctx)
}

func streamFilterLines(reader io.Reader, writer io.Writer, filter string, ctx context.Context) error {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 4*1024), 10*1024*1024)

// Pre-convert filter to bytes for faster comparison when filter is ASCII
filterBytes := []byte(filter)
isASCIIFilter := len(filterBytes) == len(filter)

for scanner.Scan() {
select {
case <-ctx.Done():
return nil
default:
}

line := scanner.Bytes()

var shouldWrite bool
if isASCIIFilter {
shouldWrite = bytes.Contains(line, filterBytes)
} else {
// For non-ASCII filters, we need string conversion for proper UTF-8 handling
shouldWrite = strings.Contains(string(line), filter)
}

if shouldWrite {
if _, err := writer.Write(line); err != nil {
return err
}
if _, err := writer.Write([]byte("\n")); err != nil {
return err
}
}
}

return scanner.Err()
}
Loading
Loading