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
13 changes: 4 additions & 9 deletions app/vlogscli/less_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ func isTerminal() bool {
}

func readWithLess(r io.Reader, disableColors, wrapLongLines bool) error {
if !isTerminal() {
// Just write everything to stdout if no terminal is available.
// Write everything to stdout if there is no terminal or 'less' isn't available in $PATH.
// The latter happens e.g. inside the distroless Docker image, which doesn't ship 'less'.
path, err := exec.LookPath("less")
if !isTerminal() || err != nil {
_, err := io.Copy(os.Stdout, r)
if err != nil && !isErrPipe(err) {
return fmt.Errorf("error when forwarding data to stdout: %w", err)
}
if err := os.Stdout.Sync(); err != nil {
return fmt.Errorf("cannot sync data to stdout: %w", err)
}
return nil
}

Expand All @@ -44,10 +43,6 @@ func readWithLess(r io.Reader, disableColors, wrapLongLines bool) error {
defer cancel()

// Start 'less' process
path, err := exec.LookPath("less")
if err != nil {
return fmt.Errorf("cannot find 'less' command: %w", err)
}
opts := []string{"less", "-F", "-X"}
if !disableColors {
opts = append(opts, "-R")
Expand Down
50 changes: 50 additions & 0 deletions app/vlogscli/less_wrapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"io"
"os"
"strings"
"testing"
)

// readWithLess must forward the response to stdout without failing when stdout
// can't be paged or synced - e.g. when it is a pipe (`vlogscli ... | grep`) or a
// terminal without the 'less' command (distroless image). os.Stdout.Sync() returns
// an error for pipes and terminals, and that error must not abort the query.
func TestReadWithLessForwardsToStdoutWithoutSyncError(t *testing.T) {
pr, pw, err := os.Pipe()
if err != nil {
t.Fatalf("cannot create pipe: %s", err)
}

// Drain the read end so io.Copy doesn't block.
type readResult struct {
data []byte
err error
}
resCh := make(chan readResult, 1)
go func() {
data, err := io.ReadAll(pr)
resCh <- readResult{data, err}
}()

origStdout := os.Stdout
os.Stdout = pw // a pipe, so os.Stdout.Sync() fails

const input = "line one\nline two\nline three\n"
err = readWithLess(strings.NewReader(input), true, false)

os.Stdout = origStdout
_ = pw.Close()
res := <-resCh

if err != nil {
t.Fatalf("unexpected error from readWithLess: %s", err)
}
if res.err != nil {
t.Fatalf("cannot read forwarded data: %s", res.err)
}
if string(res.data) != input {
t.Fatalf("unexpected stdout;\ngot:\n%q\nwant:\n%q", res.data, input)
}
}
4 changes: 4 additions & 0 deletions docs/victorialogs/querying/vlogscli.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ thanks to the way how `less` interacts with [`/select/logsql/query`](https://doc
VictoriaLogs stops query processing and frees up all the associated resources
after the response stream is closed.

If the `less` command isn't available in `$PATH` (for example, when `vlogscli` runs inside the
[distroless](https://github.com/GoogleContainerTools/distroless/) Docker image, which doesn't ship `less`),
`vlogscli` prints the query response directly to stdout without paging.

See also [`less` docs](https://man7.org/linux/man-pages/man1/less.1.html) and
[command-line integration docs for VictoriaLogs](https://docs.victoriametrics.com/victorialogs/querying/#command-line).

Expand Down