mirror of
https://github.com/containers/podman.git
synced 2025-06-23 02:18:13 +08:00
Merge pull request #19562 from Luap99/logs-tail
file logger: fix podman logs --tail with partial lines
This commit is contained in:
@ -67,16 +67,6 @@ func (c *Container) readFromLogFile(ctx context.Context, options *logs.LogOption
|
|||||||
return fmt.Errorf("unable to read log file %s for %s : %w", c.ID(), c.LogPath(), err)
|
return fmt.Errorf("unable to read log file %s for %s : %w", c.ID(), c.LogPath(), err)
|
||||||
}
|
}
|
||||||
options.WaitGroup.Add(1)
|
options.WaitGroup.Add(1)
|
||||||
if len(tailLog) > 0 {
|
|
||||||
for _, nll := range tailLog {
|
|
||||||
nll.CID = c.ID()
|
|
||||||
nll.CName = c.Name()
|
|
||||||
nll.ColorID = colorID
|
|
||||||
if nll.Since(options.Since) && nll.Until(options.Until) {
|
|
||||||
logChannel <- nll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
if options.Until.After(time.Now()) {
|
if options.Until.After(time.Now()) {
|
||||||
time.Sleep(time.Until(options.Until))
|
time.Sleep(time.Until(options.Until))
|
||||||
@ -87,6 +77,14 @@ func (c *Container) readFromLogFile(ctx context.Context, options *logs.LogOption
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
for _, nll := range tailLog {
|
||||||
|
nll.CID = c.ID()
|
||||||
|
nll.CName = c.Name()
|
||||||
|
nll.ColorID = colorID
|
||||||
|
if nll.Since(options.Since) && nll.Until(options.Until) {
|
||||||
|
logChannel <- nll
|
||||||
|
}
|
||||||
|
}
|
||||||
defer options.WaitGroup.Done()
|
defer options.WaitGroup.Done()
|
||||||
var line *tail.Line
|
var line *tail.Line
|
||||||
var ok bool
|
var ok bool
|
||||||
|
@ -85,86 +85,84 @@ func GetLogFile(path string, options *LogOptions) (*tail.Tail, []*LogLine, error
|
|||||||
|
|
||||||
func getTailLog(path string, tail int) ([]*LogLine, error) {
|
func getTailLog(path string, tail int) ([]*LogLine, error) {
|
||||||
var (
|
var (
|
||||||
nlls []*LogLine
|
|
||||||
nllCounter int
|
nllCounter int
|
||||||
leftover string
|
leftover string
|
||||||
partial string
|
|
||||||
tailLog []*LogLine
|
tailLog []*LogLine
|
||||||
|
eof bool
|
||||||
)
|
)
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer f.Close()
|
||||||
rr, err := reversereader.NewReverseReader(f)
|
rr, err := reversereader.NewReverseReader(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs := make(chan []string)
|
first := true
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
s, err := rr.Read()
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
inputs <- []string{leftover}
|
|
||||||
} else {
|
|
||||||
logrus.Error(err)
|
|
||||||
}
|
|
||||||
close(inputs)
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
logrus.Error(err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
line := strings.Split(s+leftover, "\n")
|
|
||||||
if len(line) > 1 {
|
|
||||||
inputs <- line[1:]
|
|
||||||
}
|
|
||||||
leftover = line[0]
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := range inputs {
|
for {
|
||||||
// the incoming array is FIFO; we want FIFO so
|
s, err := rr.Read()
|
||||||
// reverse the slice read order
|
if err != nil {
|
||||||
for j := len(i) - 1; j >= 0; j-- {
|
if !errors.Is(err, io.EOF) {
|
||||||
// lines that are "" are junk
|
return nil, fmt.Errorf("reverse log read: %w", err)
|
||||||
if len(i[j]) < 1 {
|
}
|
||||||
|
eof = true
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(s+leftover, "\n")
|
||||||
|
// we read a chunk of data, so make sure to read the line in inverse order
|
||||||
|
for i := len(lines) - 1; i > 0; i-- {
|
||||||
|
// ignore empty lines
|
||||||
|
if lines[i] == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// read the content in reverse and add each nll until we have the same
|
nll, err := NewLogLine(lines[i])
|
||||||
// number of F type messages as the desired tail
|
|
||||||
nll, err := NewLogLine(i[j])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nlls = append(nlls, nll)
|
if !nll.Partial() || first {
|
||||||
if !nll.Partial() {
|
|
||||||
nllCounter++
|
nllCounter++
|
||||||
|
// Even if the last line is partial we need to count it as it will be printed as line.
|
||||||
|
// Because we read backwards the first line we read is the last line in the log.
|
||||||
|
first = false
|
||||||
}
|
}
|
||||||
|
// We explicitly need to check for more lines than tail because we have
|
||||||
|
// to read to next full line and must keep all partial lines
|
||||||
|
// https://github.com/containers/podman/issues/19545
|
||||||
|
if nllCounter > tail {
|
||||||
|
// because we add lines in the inverse order we must invert the slice in the end
|
||||||
|
return reverseLog(tailLog), nil
|
||||||
|
}
|
||||||
|
// only append after the return here because we do not want to include the next full line
|
||||||
|
tailLog = append(tailLog, nll)
|
||||||
}
|
}
|
||||||
// if we have enough log lines, we can hang up
|
leftover = lines[0]
|
||||||
if nllCounter >= tail {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-assemble the log lines and trim (if needed) to the
|
// eof was reached
|
||||||
// tail length
|
if eof {
|
||||||
for _, nll := range nlls {
|
// when we have still a line and do not have enough tail lines already
|
||||||
if nll.Partial() {
|
if leftover != "" && nllCounter < tail {
|
||||||
partial += nll.Msg
|
nll, err := NewLogLine(leftover)
|
||||||
} else {
|
if err != nil {
|
||||||
nll.Msg += partial
|
return nil, err
|
||||||
// prepend because we need to reverse the order again to FIFO
|
}
|
||||||
tailLog = append([]*LogLine{nll}, tailLog...)
|
tailLog = append(tailLog, nll)
|
||||||
partial = ""
|
}
|
||||||
}
|
// because we add lines in the inverse order we must invert the slice in the end
|
||||||
if len(tailLog) == tail {
|
return reverseLog(tailLog), nil
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tailLog, nil
|
}
|
||||||
|
|
||||||
|
// reverseLog reverse the log line slice, needed for tail as we read lines backwards but still
|
||||||
|
// need to print them in the correct order at the end so use that helper for it.
|
||||||
|
func reverseLog(s []*LogLine) []*LogLine {
|
||||||
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// getColor returns an ANSI escape code for color based on the colorID
|
// getColor returns an ANSI escape code for color based on the colorID
|
||||||
|
137
libpod/logs/log_test.go
Normal file
137
libpod/logs/log_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logTime time.Time
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logTime, _ = time.Parse(LogTimeFormat, "2023-08-07T19:56:34.223758260-06:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTestLogLine(typ, msg string) *LogLine {
|
||||||
|
return &LogLine{
|
||||||
|
Device: "stdout",
|
||||||
|
ParseLogType: typ,
|
||||||
|
Msg: msg,
|
||||||
|
Time: logTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTailLog(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fileContent string
|
||||||
|
tail int
|
||||||
|
want []*LogLine
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple tail",
|
||||||
|
fileContent: `2023-08-07T19:56:34.223758260-06:00 stdout F line1
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line2
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line3
|
||||||
|
`,
|
||||||
|
tail: 2,
|
||||||
|
want: []*LogLine{makeTestLogLine("F", "line2"), makeTestLogLine("F", "line3")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple tail with more tail than lines",
|
||||||
|
fileContent: `2023-08-07T19:56:34.223758260-06:00 stdout F line1
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line2
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line3
|
||||||
|
`,
|
||||||
|
tail: 10,
|
||||||
|
want: []*LogLine{makeTestLogLine("F", "line1"), makeTestLogLine("F", "line2"), makeTestLogLine("F", "line3")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tail with partial logs",
|
||||||
|
fileContent: `2023-08-07T19:56:34.223758260-06:00 stdout F line1
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P lin
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F e2
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line3
|
||||||
|
`,
|
||||||
|
tail: 2,
|
||||||
|
want: []*LogLine{makeTestLogLine("P", "lin"), makeTestLogLine("F", "e2"), makeTestLogLine("F", "line3")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tail with partial logs and more than lines",
|
||||||
|
fileContent: `2023-08-07T19:56:34.223758260-06:00 stdout F line1
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P lin
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F e2
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line3
|
||||||
|
`,
|
||||||
|
tail: 10,
|
||||||
|
want: []*LogLine{makeTestLogLine("F", "line1"), makeTestLogLine("P", "lin"), makeTestLogLine("F", "e2"), makeTestLogLine("F", "line3")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple partial lines in a row",
|
||||||
|
fileContent: `2023-08-07T19:56:34.223758260-06:00 stdout F line1
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P l
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P i
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P n
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P e
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F 2
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F line3
|
||||||
|
`,
|
||||||
|
tail: 2,
|
||||||
|
want: []*LogLine{makeTestLogLine("P", "l"), makeTestLogLine("P", "i"), makeTestLogLine("P", "n"),
|
||||||
|
makeTestLogLine("P", "e"), makeTestLogLine("F", "2"), makeTestLogLine("F", "line3")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial line at the end",
|
||||||
|
fileContent: `2023-08-07T19:56:34.223758260-06:00 stdout F line1
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout P lin
|
||||||
|
`,
|
||||||
|
tail: 1,
|
||||||
|
want: []*LogLine{makeTestLogLine("P", "lin")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
file := filepath.Join(dir, "log")
|
||||||
|
f, err := os.Create(file)
|
||||||
|
assert.NoError(t, err, "create log file")
|
||||||
|
_, err = f.WriteString(tt.fileContent)
|
||||||
|
assert.NoError(t, err, "write log file")
|
||||||
|
f.Close()
|
||||||
|
got, err := getTailLog(file, tt.tail)
|
||||||
|
assert.NoError(t, err, "getTailLog()")
|
||||||
|
assert.Equal(t, tt.want, got, "log lines")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTailLogBigFiles(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
file := filepath.Join(dir, "log")
|
||||||
|
f, err := os.Create(file)
|
||||||
|
assert.NoError(t, err, "create log file")
|
||||||
|
want := make([]*LogLine, 0, 2000)
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
_, err = f.WriteString(`2023-08-07T19:56:34.223758260-06:00 stdout P lin
|
||||||
|
2023-08-07T19:56:34.223758260-06:00 stdout F e2
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err, "write log file")
|
||||||
|
want = append(want, makeTestLogLine("P", "lin"), makeTestLogLine("F", "e2"))
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// try a big tail greater than the lines
|
||||||
|
got, err := getTailLog(file, 5000)
|
||||||
|
assert.NoError(t, err, "getTailLog()")
|
||||||
|
assert.Equal(t, want, got, "all log lines")
|
||||||
|
|
||||||
|
// try a smaller than lines tail
|
||||||
|
got, err = getTailLog(file, 100)
|
||||||
|
assert.NoError(t, err, "getTailLog()")
|
||||||
|
// this will return the last 200 lines because of partial + full and we only count full lines for tail.
|
||||||
|
assert.Equal(t, want[1800:2000], got, "tail 100 log lines")
|
||||||
|
}
|
@ -57,10 +57,7 @@ func (r *ReverseReader) Read() (string, error) {
|
|||||||
if err != nil && !errors.Is(err, io.EOF) {
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if int64(n) < r.readSize {
|
|
||||||
b = b[0:n]
|
|
||||||
}
|
|
||||||
// Move the offset one pagesize up
|
// Move the offset one pagesize up
|
||||||
r.offset -= r.readSize
|
r.offset -= r.readSize
|
||||||
return string(b), nil
|
return string(b[:n]), nil
|
||||||
}
|
}
|
||||||
|
@ -1229,10 +1229,7 @@ func (ic *ContainerEngine) ContainerLogs(ctx context.Context, namesOrIds []strin
|
|||||||
WaitGroup: &wg,
|
WaitGroup: &wg,
|
||||||
}
|
}
|
||||||
|
|
||||||
chSize := len(containers) * int(options.Tail)
|
chSize := len(containers)
|
||||||
if chSize <= 0 {
|
|
||||||
chSize = 1
|
|
||||||
}
|
|
||||||
logChannel := make(chan *logs.LogLine, chSize)
|
logChannel := make(chan *logs.LogLine, chSize)
|
||||||
|
|
||||||
libpodContainers := make([]*libpod.Container, len(containers))
|
libpodContainers := make([]*libpod.Container, len(containers))
|
||||||
|
@ -387,4 +387,46 @@ $content--2.*" "logs --until -f on running container works"
|
|||||||
|
|
||||||
_log_test_follow_until journald
|
_log_test_follow_until journald
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://github.com/containers/podman/issues/19545
|
||||||
|
@test "podman logs --tail, k8s-file with partial lines" {
|
||||||
|
cname="tail_container"
|
||||||
|
|
||||||
|
# "-t" gives us ^Ms (CRs) in the log
|
||||||
|
run_podman run --name $cname --log-driver k8s-file -t $IMAGE echo hi
|
||||||
|
|
||||||
|
# Hand-craft a log file with partial lines and carriage returns
|
||||||
|
run_podman inspect --format '{{.HostConfig.LogConfig.Path}}' $cname
|
||||||
|
logpath="$output"
|
||||||
|
timestamp=$(awk '{print $1}' <"$logpath")
|
||||||
|
cr=$'\r'
|
||||||
|
nl=$'\n'
|
||||||
|
cat >| $logpath <<EOF
|
||||||
|
$timestamp stdout F podman1$cr
|
||||||
|
$timestamp stdout P podman2
|
||||||
|
$timestamp stdout F $cr
|
||||||
|
$timestamp stdout F podman3$cr
|
||||||
|
EOF
|
||||||
|
|
||||||
|
expect1="podman3${cr}"
|
||||||
|
expect2="podman2${cr}${nl}podman3${cr}"
|
||||||
|
expect3="podman1${cr}${nl}podman2${cr}${nl}podman3${cr}"
|
||||||
|
|
||||||
|
# This always worked
|
||||||
|
run_podman logs --tail 1 $cname
|
||||||
|
assert "$output" = "$expect1" "--tail 1"
|
||||||
|
|
||||||
|
# Prior to this PR, the first line would be "^M" without the podman
|
||||||
|
run_podman logs --tail 2 $cname
|
||||||
|
assert "$output" = "$expect2" "--tail 2"
|
||||||
|
|
||||||
|
# Confirm that we won't overrun
|
||||||
|
for i in 3 4 5; do
|
||||||
|
run_podman logs --tail $i $cname
|
||||||
|
assert "$output" = "$expect3" "--tail $i"
|
||||||
|
done
|
||||||
|
|
||||||
|
run_podman rm $cname
|
||||||
|
}
|
||||||
|
|
||||||
# vim: filetype=sh
|
# vim: filetype=sh
|
||||||
|
Reference in New Issue
Block a user