1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00
moby--moby/daemon/logger/local/local_test.go
Cory Snider 6d5bc07189 daemon/logger: fix refcounting decompressed files
The refCounter used for sharing temporary decompressed log files and
tracking when the files can be deleted is keyed off the source file's
path. But the path of a log file is not stable: it is renamed on each
rotation. Consequently, when logging is configured with both rotation
and compression, multiple concurrent readers of a container's logs could
read logs out of order, see duplicates or decompress a log file which
has already been decompressed.

Replace refCounter with a new implementation, sharedTempFileConverter,
which is agnostic to the file path, keying off the source file's
identity instead. Additionally, sharedTempFileConverter handles the full
lifecycle of the temporary file, from creation to deletion. This is all
abstracted from the consumer: all the bookkeeping and cleanup is handled
behind the scenes when Close() is called on the returned reader value.
Only one file descriptor is used per temporary file, which is shared by
all readers.

A channel is used for concurrency control so that the lock can be
acquired inside a select statement. While not currently utilized, this
makes it possible to add support for cancellation to
sharedTempFileConverter in the future.

Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-05-19 15:22:22 -04:00

143 lines
3.8 KiB
Go

package local
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/plugins/logdriver"
"github.com/docker/docker/daemon/logger"
"github.com/docker/docker/daemon/logger/loggertest"
protoio "github.com/gogo/protobuf/io"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestWriteLog(t *testing.T) {
t.Parallel()
dir, err := os.MkdirTemp("", t.Name())
assert.NilError(t, err)
defer os.RemoveAll(dir)
logPath := filepath.Join(dir, "test.log")
l, err := New(logger.Info{LogPath: logPath})
assert.NilError(t, err)
defer l.Close()
m1 := logger.Message{Source: "stdout", Timestamp: time.Now().Add(-1 * 30 * time.Minute), Line: []byte("message 1")}
m2 := logger.Message{Source: "stdout", Timestamp: time.Now().Add(-1 * 20 * time.Minute), Line: []byte("message 2"), PLogMetaData: &backend.PartialLogMetaData{Last: true, ID: "0001", Ordinal: 1}}
m3 := logger.Message{Source: "stderr", Timestamp: time.Now().Add(-1 * 10 * time.Minute), Line: []byte("message 3")}
// copy the log message because the underying log writer resets the log message and returns it to a buffer pool
err = l.Log(copyLogMessage(&m1))
assert.NilError(t, err)
err = l.Log(copyLogMessage(&m2))
assert.NilError(t, err)
err = l.Log(copyLogMessage(&m3))
assert.NilError(t, err)
f, err := os.Open(logPath)
assert.NilError(t, err)
defer f.Close()
dec := protoio.NewUint32DelimitedReader(f, binary.BigEndian, 1e6)
var (
proto logdriver.LogEntry
testProto logdriver.LogEntry
partial logdriver.PartialLogEntryMetadata
)
lenBuf := make([]byte, encodeBinaryLen)
seekMsgLen := func() {
io.ReadFull(f, lenBuf)
}
err = dec.ReadMsg(&proto)
assert.NilError(t, err)
messageToProto(&m1, &testProto, &partial)
assert.Check(t, is.DeepEqual(testProto, proto), "expected:\n%+v\ngot:\n%+v", testProto, proto)
seekMsgLen()
err = dec.ReadMsg(&proto)
assert.NilError(t, err)
messageToProto(&m2, &testProto, &partial)
assert.Check(t, is.DeepEqual(testProto, proto))
seekMsgLen()
err = dec.ReadMsg(&proto)
assert.NilError(t, err)
messageToProto(&m3, &testProto, &partial)
assert.Check(t, is.DeepEqual(testProto, proto), "expected:\n%+v\ngot:\n%+v", testProto, proto)
}
func TestReadLog(t *testing.T) {
r := loggertest.Reader{
Factory: func(t *testing.T, info logger.Info) func(*testing.T) logger.Logger {
dir := t.TempDir()
info.LogPath = filepath.Join(dir, info.ContainerID+".log")
return func(t *testing.T) logger.Logger {
l, err := New(info)
assert.NilError(t, err)
return l
}
},
}
t.Run("Tail", r.TestTail)
t.Run("Follow", r.TestFollow)
}
func BenchmarkLogWrite(b *testing.B) {
f, err := os.CreateTemp("", b.Name())
assert.Assert(b, err)
defer os.Remove(f.Name())
f.Close()
local, err := New(logger.Info{LogPath: f.Name()})
assert.Assert(b, err)
defer local.Close()
t := time.Now().UTC()
for _, data := range [][]byte{
[]byte(""),
[]byte("a short string"),
bytes.Repeat([]byte("a long string"), 100),
bytes.Repeat([]byte("a really long string"), 10000),
} {
b.Run(fmt.Sprintf("%d", len(data)), func(b *testing.B) {
entry := &logdriver.LogEntry{Line: data, Source: "stdout", TimeNano: t.UnixNano()}
b.SetBytes(int64(entry.Size() + encodeBinaryLen + encodeBinaryLen))
b.ResetTimer()
for i := 0; i < b.N; i++ {
msg := logger.NewMessage()
msg.Line = data
msg.Timestamp = t
msg.Source = "stdout"
if err := local.Log(msg); err != nil {
b.Fatal(err)
}
}
})
}
}
func copyLogMessage(src *logger.Message) *logger.Message {
dst := logger.NewMessage()
dst.Source = src.Source
dst.Timestamp = src.Timestamp
dst.Attrs = src.Attrs
dst.Err = src.Err
dst.Line = append(dst.Line, src.Line...)
if src.PLogMetaData != nil {
lmd := *src.PLogMetaData
dst.PLogMetaData = &lmd
}
return dst
}