2018-02-05 21:05:59 +00:00
|
|
|
package loggerutils // import "github.com/docker/docker/daemon/logger/loggerutils"
|
2017-07-18 15:54:03 +00:00
|
|
|
|
|
|
|
import (
|
2018-03-13 02:47:24 +00:00
|
|
|
"compress/gzip"
|
2017-07-18 15:54:03 +00:00
|
|
|
"context"
|
2018-03-13 02:47:24 +00:00
|
|
|
"encoding/json"
|
2017-07-18 15:54:03 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2022-02-15 16:31:18 +00:00
|
|
|
"io/fs"
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
"math"
|
2017-07-18 15:54:03 +00:00
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/docker/docker/daemon/logger"
|
2018-03-13 02:47:24 +00:00
|
|
|
"github.com/docker/docker/pkg/pools"
|
2017-07-18 15:54:03 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
)
|
|
|
|
|
2018-03-13 02:47:24 +00:00
|
|
|
// rotateFileMetadata is a metadata of the gzip header of the compressed log file
|
|
|
|
type rotateFileMetadata struct {
|
|
|
|
LastTime time.Time `json:"lastTime,omitempty"`
|
|
|
|
}
|
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
// LogFile is Logger implementation for default Docker logging.
|
|
|
|
type LogFile struct {
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
mu sync.Mutex // protects the logfile access
|
|
|
|
closed chan struct{}
|
|
|
|
rotateMu sync.Mutex // blocks the next rotation until the current rotation is completed
|
|
|
|
// Lock out readers while performing a non-atomic sequence of filesystem
|
|
|
|
// operations (RLock: open, Lock: rename, delete).
|
|
|
|
//
|
|
|
|
// fsopMu should be locked for writing only while holding rotateMu.
|
|
|
|
fsopMu sync.RWMutex
|
|
|
|
|
|
|
|
// Logger configuration
|
|
|
|
|
|
|
|
capacity int64 // maximum size of each file
|
|
|
|
maxFiles int // maximum number of files
|
|
|
|
compress bool // whether old versions of log files are compressed
|
|
|
|
perms os.FileMode
|
|
|
|
|
|
|
|
// Log file codec
|
|
|
|
|
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-02-23 18:44:15 +00:00
|
|
|
marshal logger.MarshalFunc
|
|
|
|
createDecoder MakeDecoderFn
|
|
|
|
getTailReader GetTailReaderFunc
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
|
|
|
|
// Log reader state in a 1-buffered channel.
|
|
|
|
//
|
|
|
|
// Share memory by communicating: receive to acquire, send to release.
|
|
|
|
// The state struct is passed around by value so that use-after-send
|
|
|
|
// bugs cannot escalate to data races.
|
|
|
|
//
|
|
|
|
// A method which receives the state value takes ownership of it. The
|
|
|
|
// owner is responsible for either passing ownership along or sending
|
|
|
|
// the state back to the channel. By convention, the semantics of
|
|
|
|
// passing along ownership is expressed with function argument types.
|
|
|
|
// Methods which take a pointer *logReadState argument borrow the state,
|
|
|
|
// analogous to functions which require a lock to be held when calling.
|
|
|
|
// The caller retains ownership. Calling a method which which takes a
|
|
|
|
// value logFileState argument gives ownership to the callee.
|
|
|
|
read chan logReadState
|
|
|
|
|
|
|
|
decompress *sharedTempFileConverter
|
|
|
|
|
|
|
|
pos logPos // Current log file write position.
|
|
|
|
f *os.File // Current log file for writing.
|
|
|
|
lastTimestamp time.Time // timestamp of the last log
|
|
|
|
}
|
|
|
|
|
|
|
|
type logPos struct {
|
|
|
|
// Size of the current file.
|
|
|
|
size int64
|
|
|
|
// File rotation sequence number (modulo 2**16).
|
|
|
|
rotation uint16
|
|
|
|
}
|
|
|
|
|
|
|
|
type logReadState struct {
|
|
|
|
// Current log file position.
|
|
|
|
pos logPos
|
|
|
|
// Wait list to be notified of the value of pos next time it changes.
|
|
|
|
wait []chan<- logPos
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
|
2020-04-08 19:24:31 +00:00
|
|
|
// MakeDecoderFn creates a decoder
|
|
|
|
type MakeDecoderFn func(rdr io.Reader) Decoder
|
|
|
|
|
|
|
|
// Decoder is for reading logs
|
|
|
|
// It is created by the log reader by calling the `MakeDecoderFunc`
|
|
|
|
type Decoder interface {
|
|
|
|
// Reset resets the decoder
|
|
|
|
// Reset is called for certain events, such as log rotations
|
|
|
|
Reset(io.Reader)
|
|
|
|
// Decode decodes the next log messeage from the stream
|
|
|
|
Decode() (*logger.Message, error)
|
|
|
|
// Close signals to the decoder that it can release whatever resources it was using.
|
|
|
|
Close()
|
|
|
|
}
|
2017-07-18 15:54:03 +00:00
|
|
|
|
2018-04-05 16:39:28 +00:00
|
|
|
// SizeReaderAt defines a ReaderAt that also reports its size.
|
|
|
|
// This is used for tailing log files.
|
|
|
|
type SizeReaderAt interface {
|
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-02-23 18:44:15 +00:00
|
|
|
io.Reader
|
2018-04-05 16:39:28 +00:00
|
|
|
io.ReaderAt
|
|
|
|
Size() int64
|
|
|
|
}
|
|
|
|
|
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-02-23 18:44:15 +00:00
|
|
|
type readAtCloser interface {
|
|
|
|
io.ReaderAt
|
|
|
|
io.Closer
|
|
|
|
}
|
|
|
|
|
2018-04-05 16:39:28 +00:00
|
|
|
// GetTailReaderFunc is used to truncate a reader to only read as much as is required
|
|
|
|
// in order to get the passed in number of log lines.
|
|
|
|
// It returns the sectioned reader, the number of lines that the section reader
|
|
|
|
// contains, and any error that occurs.
|
|
|
|
type GetTailReaderFunc func(ctx context.Context, f SizeReaderAt, nLogLines int) (rdr io.Reader, nLines int, err error)
|
|
|
|
|
2018-05-07 15:25:41 +00:00
|
|
|
// NewLogFile creates new LogFile
|
2020-04-08 19:24:31 +00:00
|
|
|
func NewLogFile(logPath string, capacity int64, maxFiles int, compress bool, marshaller logger.MarshalFunc, decodeFunc MakeDecoderFn, perms os.FileMode, getTailReader GetTailReaderFunc) (*LogFile, error) {
|
2019-09-23 18:09:01 +00:00
|
|
|
log, err := openFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms)
|
2017-07-18 15:54:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-11-25 17:53:03 +00:00
|
|
|
size, err := log.Seek(0, io.SeekEnd)
|
2017-07-18 15:54:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
pos := logPos{
|
|
|
|
size: size,
|
|
|
|
// Force a wraparound on first rotation to shake out any
|
|
|
|
// modular-arithmetic bugs.
|
|
|
|
rotation: math.MaxUint16,
|
|
|
|
}
|
|
|
|
st := make(chan logReadState, 1)
|
|
|
|
st <- logReadState{pos: pos}
|
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
return &LogFile{
|
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-02-23 18:44:15 +00:00
|
|
|
f: log,
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
read: st,
|
|
|
|
pos: pos,
|
|
|
|
closed: make(chan struct{}),
|
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-02-23 18:44:15 +00:00
|
|
|
capacity: capacity,
|
|
|
|
maxFiles: maxFiles,
|
|
|
|
compress: compress,
|
|
|
|
decompress: newSharedTempFileConverter(decompress),
|
|
|
|
marshal: marshaller,
|
|
|
|
createDecoder: decodeFunc,
|
|
|
|
perms: perms,
|
|
|
|
getTailReader: getTailReader,
|
2017-07-18 15:54:03 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteLogEntry writes the provided log message to the current log file.
|
|
|
|
// This may trigger a rotation event if the max file/capacity limits are hit.
|
|
|
|
func (w *LogFile) WriteLogEntry(msg *logger.Message) error {
|
|
|
|
b, err := w.marshal(msg)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error marshalling log message")
|
|
|
|
}
|
|
|
|
|
2022-03-01 18:34:32 +00:00
|
|
|
ts := msg.Timestamp
|
2017-07-18 15:54:03 +00:00
|
|
|
logger.PutMessage(msg)
|
2022-03-01 18:34:32 +00:00
|
|
|
msg = nil // Turn use-after-put bugs into panics.
|
2017-07-18 15:54:03 +00:00
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
select {
|
|
|
|
case <-w.closed:
|
2017-07-18 15:54:03 +00:00
|
|
|
return errors.New("cannot write because the output file was closed")
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
default:
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
w.mu.Lock()
|
|
|
|
defer w.mu.Unlock()
|
2017-07-18 15:54:03 +00:00
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// Are we due for a rotation?
|
|
|
|
if w.capacity != -1 && w.pos.size >= w.capacity {
|
|
|
|
if err := w.rotate(); err != nil {
|
|
|
|
return errors.Wrap(err, "error rotating log file")
|
|
|
|
}
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
n, err := w.f.Write(b)
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error writing log entry")
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
w.pos.size += int64(n)
|
|
|
|
w.lastTimestamp = ts
|
2020-06-12 23:01:37 +00:00
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// Notify any waiting readers that there is a new log entry to read.
|
|
|
|
st := <-w.read
|
|
|
|
defer func() { w.read <- st }()
|
|
|
|
st.pos = w.pos
|
2017-07-18 15:54:03 +00:00
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
for _, c := range st.wait {
|
|
|
|
c <- st.pos
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// Optimization: retain the backing array to save a heap allocation next
|
|
|
|
// time a reader appends to the list.
|
|
|
|
if st.wait != nil {
|
|
|
|
st.wait = st.wait[:0]
|
2020-07-15 16:56:28 +00:00
|
|
|
}
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-07-15 16:56:28 +00:00
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
func (w *LogFile) rotate() (retErr error) {
|
2020-07-15 16:56:28 +00:00
|
|
|
w.rotateMu.Lock()
|
2020-06-12 23:01:37 +00:00
|
|
|
noCompress := w.maxFiles <= 1 || !w.compress
|
2020-07-15 16:56:28 +00:00
|
|
|
defer func() {
|
2020-06-12 23:01:37 +00:00
|
|
|
// If we aren't going to run the goroutine to compress the log file, then we need to unlock in this function.
|
|
|
|
// Otherwise the lock will be released in the goroutine that handles compression.
|
|
|
|
if retErr != nil || noCompress {
|
2018-03-13 02:47:24 +00:00
|
|
|
w.rotateMu.Unlock()
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
2020-07-15 16:56:28 +00:00
|
|
|
}()
|
2018-03-13 02:47:24 +00:00
|
|
|
|
2020-07-15 16:56:28 +00:00
|
|
|
fname := w.f.Name()
|
|
|
|
if err := w.f.Close(); err != nil {
|
|
|
|
// if there was an error during a prior rotate, the file could already be closed
|
2022-02-15 16:31:18 +00:00
|
|
|
if !errors.Is(err, fs.ErrClosed) {
|
2020-07-15 16:56:28 +00:00
|
|
|
return errors.Wrap(err, "error closing file")
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
2020-07-15 16:56:28 +00:00
|
|
|
}
|
2018-03-13 02:47:24 +00:00
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
file, err := func() (*os.File, error) {
|
|
|
|
w.fsopMu.Lock()
|
|
|
|
defer w.fsopMu.Unlock()
|
|
|
|
|
|
|
|
if err := rotate(fname, w.maxFiles, w.compress); err != nil {
|
|
|
|
logrus.WithError(err).Warn("Error rotating log file, log data may have been lost")
|
2022-02-15 16:31:18 +00:00
|
|
|
} else {
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// We may have readers working their way through the
|
|
|
|
// current log file so we can't truncate it. We need to
|
|
|
|
// start writing new logs to an empty file with the same
|
|
|
|
// name as the current one so we need to rotate the
|
|
|
|
// current file out of the way.
|
|
|
|
if w.maxFiles < 2 {
|
|
|
|
if err := unlink(fname); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
|
|
logrus.WithError(err).Error("Error unlinking current log file")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := os.Rename(fname, fname+".1"); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
|
|
logrus.WithError(err).Error("Error renaming current log file")
|
|
|
|
}
|
2020-06-12 23:01:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// Notwithstanding the above, open with the truncate flag anyway
|
|
|
|
// in case rotation didn't work out as planned.
|
|
|
|
return openFile(fname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, w.perms)
|
|
|
|
}()
|
2020-07-15 16:56:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
w.f = file
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
w.pos = logPos{rotation: w.pos.rotation + 1}
|
2020-06-12 23:01:37 +00:00
|
|
|
|
|
|
|
if noCompress {
|
2020-07-15 16:56:28 +00:00
|
|
|
return nil
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
|
2020-07-15 16:56:28 +00:00
|
|
|
ts := w.lastTimestamp
|
|
|
|
go func() {
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
defer w.rotateMu.Unlock()
|
|
|
|
// No need to hold fsopMu as at no point will the filesystem be
|
|
|
|
// in a state which would cause problems for readers. Opening
|
|
|
|
// the uncompressed file is tried first, falling back to the
|
|
|
|
// compressed one. compressFile only deletes the uncompressed
|
|
|
|
// file once the compressed one is fully written out, so at no
|
|
|
|
// point during the compression process will a reader fail to
|
|
|
|
// open a complete copy of the file.
|
2020-07-15 16:56:28 +00:00
|
|
|
if err := compressFile(fname+".1", ts); err != nil {
|
|
|
|
logrus.WithError(err).Error("Error compressing log file after rotation")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-13 02:47:24 +00:00
|
|
|
func rotate(name string, maxFiles int, compress bool) error {
|
2017-07-18 15:54:03 +00:00
|
|
|
if maxFiles < 2 {
|
|
|
|
return nil
|
|
|
|
}
|
2018-03-13 02:47:24 +00:00
|
|
|
|
|
|
|
var extension string
|
|
|
|
if compress {
|
|
|
|
extension = ".gz"
|
|
|
|
}
|
2018-05-07 15:25:41 +00:00
|
|
|
|
|
|
|
lastFile := fmt.Sprintf("%s.%d%s", name, maxFiles-1, extension)
|
2022-02-15 16:31:18 +00:00
|
|
|
err := unlink(lastFile)
|
|
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
2018-05-07 15:25:41 +00:00
|
|
|
return errors.Wrap(err, "error removing oldest log file")
|
|
|
|
}
|
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
for i := maxFiles - 1; i > 1; i-- {
|
2018-03-13 02:47:24 +00:00
|
|
|
toPath := name + "." + strconv.Itoa(i) + extension
|
|
|
|
fromPath := name + "." + strconv.Itoa(i-1) + extension
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
err := os.Rename(fromPath, toPath)
|
|
|
|
logrus.WithError(err).WithField("source", fromPath).WithField("target", toPath).Trace("Rotating log file")
|
|
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
2018-03-13 02:47:24 +00:00
|
|
|
return err
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-15 16:56:28 +00:00
|
|
|
func compressFile(fileName string, lastTimestamp time.Time) (retErr error) {
|
2020-07-15 17:40:21 +00:00
|
|
|
file, err := open(fileName)
|
2018-03-13 02:47:24 +00:00
|
|
|
if err != nil {
|
2022-02-15 16:31:18 +00:00
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
2020-07-15 16:56:28 +00:00
|
|
|
logrus.WithField("file", fileName).WithError(err).Debug("Could not open log file to compress")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return errors.Wrap(err, "failed to open log file")
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
file.Close()
|
2020-06-12 23:01:37 +00:00
|
|
|
if retErr == nil {
|
2022-02-15 16:31:18 +00:00
|
|
|
err := unlink(fileName)
|
|
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
2020-06-12 23:01:37 +00:00
|
|
|
retErr = errors.Wrap(err, "failed to remove source log file")
|
|
|
|
}
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2019-09-23 18:09:01 +00:00
|
|
|
outFile, err := openFile(fileName+".gz", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0640)
|
2018-03-13 02:47:24 +00:00
|
|
|
if err != nil {
|
2020-07-15 16:56:28 +00:00
|
|
|
return errors.Wrap(err, "failed to open or create gzip log file")
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
outFile.Close()
|
2020-07-15 16:56:28 +00:00
|
|
|
if retErr != nil {
|
2022-02-15 16:31:18 +00:00
|
|
|
if err := unlink(fileName + ".gz"); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
2020-07-15 16:56:28 +00:00
|
|
|
logrus.WithError(err).Error("Error cleaning up after failed log compression")
|
|
|
|
}
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
compressWriter := gzip.NewWriter(outFile)
|
|
|
|
defer compressWriter.Close()
|
|
|
|
|
2019-09-20 22:50:10 +00:00
|
|
|
// Add the last log entry timestamp to the gzip header
|
2018-03-13 02:47:24 +00:00
|
|
|
extra := rotateFileMetadata{}
|
|
|
|
extra.LastTime = lastTimestamp
|
|
|
|
compressWriter.Header.Extra, err = json.Marshal(&extra)
|
|
|
|
if err != nil {
|
|
|
|
// Here log the error only and don't return since this is just an optimization.
|
2018-05-07 15:25:41 +00:00
|
|
|
logrus.Warningf("Failed to marshal gzip header as JSON: %v", err)
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = pools.Copy(compressWriter, file)
|
|
|
|
if err != nil {
|
2020-07-15 16:56:28 +00:00
|
|
|
return errors.Wrapf(err, "error compressing log file %s", fileName)
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
2020-07-15 16:56:28 +00:00
|
|
|
|
|
|
|
return nil
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
// MaxFiles return maximum number of files
|
|
|
|
func (w *LogFile) MaxFiles() int {
|
|
|
|
return w.maxFiles
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close closes underlying file and signals all readers to stop.
|
|
|
|
func (w *LogFile) Close() error {
|
|
|
|
w.mu.Lock()
|
|
|
|
defer w.mu.Unlock()
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
select {
|
|
|
|
case <-w.closed:
|
2017-07-18 15:54:03 +00:00
|
|
|
return nil
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
default:
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
2022-02-15 16:31:18 +00:00
|
|
|
if err := w.f.Close(); err != nil && !errors.Is(err, fs.ErrClosed) {
|
2017-07-18 15:54:03 +00:00
|
|
|
return err
|
|
|
|
}
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
close(w.closed)
|
|
|
|
// Wait until any in-progress rotation is complete.
|
|
|
|
w.rotateMu.Lock()
|
|
|
|
w.rotateMu.Unlock() //nolint:staticcheck
|
2017-07-18 15:54:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// ReadLogs decodes entries from log files.
|
2018-05-07 15:25:41 +00:00
|
|
|
//
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// It is the caller's responsibility to call ConsumerGone on the LogWatcher.
|
2022-03-02 23:16:47 +00:00
|
|
|
func (w *LogFile) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
|
|
|
|
watcher := logger.NewLogWatcher()
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// Lock out filesystem operations so that we can capture the read
|
|
|
|
// position and atomically open the corresponding log file, without the
|
|
|
|
// file getting rotated out from under us.
|
|
|
|
w.fsopMu.RLock()
|
|
|
|
// Capture the read position synchronously to ensure that we start
|
|
|
|
// following from the last entry logged before ReadLogs was called,
|
|
|
|
// which is required for flake-free unit testing.
|
|
|
|
st := <-w.read
|
|
|
|
pos := st.pos
|
|
|
|
w.read <- st
|
|
|
|
go w.readLogsLocked(pos, config, watcher)
|
2022-03-02 23:16:47 +00:00
|
|
|
return watcher
|
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// readLogsLocked is the bulk of the implementation of ReadLogs.
|
|
|
|
//
|
|
|
|
// w.fsopMu must be locked for reading when calling this method.
|
|
|
|
// w.fsopMu.RUnlock() is called before returning.
|
|
|
|
func (w *LogFile) readLogsLocked(currentPos logPos, config logger.ReadConfig, watcher *logger.LogWatcher) {
|
2022-03-02 23:38:16 +00:00
|
|
|
defer close(watcher.Msg)
|
2022-03-02 23:16:47 +00:00
|
|
|
|
2020-07-15 17:40:21 +00:00
|
|
|
currentFile, err := open(w.f.Name())
|
2017-07-18 15:54:03 +00:00
|
|
|
if err != nil {
|
|
|
|
watcher.Err <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer currentFile.Close()
|
|
|
|
|
2020-04-08 19:24:31 +00:00
|
|
|
dec := w.createDecoder(nil)
|
|
|
|
defer dec.Close()
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
currentChunk := io.NewSectionReader(currentFile, 0, currentPos.size)
|
2020-06-12 23:01:37 +00:00
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
if config.Tail != 0 {
|
2018-04-05 16:39:28 +00:00
|
|
|
// TODO(@cpuguy83): Instead of opening every file, only get the files which
|
|
|
|
// are needed to tail.
|
|
|
|
// This is especially costly when compression is enabled.
|
2018-03-13 02:47:24 +00:00
|
|
|
files, err := w.openRotatedFiles(config)
|
|
|
|
if err != nil {
|
|
|
|
watcher.Err <- err
|
|
|
|
return
|
|
|
|
}
|
2018-04-05 16:39:28 +00:00
|
|
|
|
|
|
|
closeFiles := func() {
|
|
|
|
for _, f := range files {
|
|
|
|
f.Close()
|
|
|
|
}
|
2018-02-13 20:03:56 +00:00
|
|
|
}
|
2018-04-05 16:39:28 +00:00
|
|
|
|
|
|
|
readers := make([]SizeReaderAt, 0, len(files)+1)
|
2018-03-13 02:47:24 +00:00
|
|
|
for _, f := range files {
|
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-02-23 18:44:15 +00:00
|
|
|
switch ff := f.(type) {
|
|
|
|
case SizeReaderAt:
|
|
|
|
readers = append(readers, ff)
|
|
|
|
case interface{ Stat() (fs.FileInfo, error) }:
|
|
|
|
stat, err := ff.Stat()
|
|
|
|
if err != nil {
|
|
|
|
watcher.Err <- errors.Wrap(err, "error reading size of rotated file")
|
|
|
|
closeFiles()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
readers = append(readers, io.NewSectionReader(f, 0, stat.Size()))
|
|
|
|
default:
|
|
|
|
panic(fmt.Errorf("rotated file value %#v (%[1]T) has neither Size() nor Stat() methods", f))
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
}
|
2018-04-05 16:39:28 +00:00
|
|
|
if currentChunk.Size() > 0 {
|
|
|
|
readers = append(readers, currentChunk)
|
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
ok := tailFiles(readers, watcher, dec, w.getTailReader, config)
|
2018-04-05 16:39:28 +00:00
|
|
|
closeFiles()
|
2020-06-12 23:01:37 +00:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
} else {
|
|
|
|
w.fsopMu.RUnlock()
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
|
2022-03-02 22:33:39 +00:00
|
|
|
if !config.Follow {
|
2017-07-18 15:54:03 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
(&follow{
|
|
|
|
LogFile: w,
|
|
|
|
Watcher: watcher,
|
|
|
|
Decoder: dec,
|
|
|
|
Since: config.Since,
|
|
|
|
Until: config.Until,
|
|
|
|
}).Do(currentFile, currentPos)
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
// openRotatedFiles returns a slice of files open for reading, in order from
|
|
|
|
// oldest to newest, and calls w.fsopMu.RUnlock() before returning.
|
|
|
|
//
|
|
|
|
// This method must only be called with w.fsopMu locked for reading.
|
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-02-23 18:44:15 +00:00
|
|
|
func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []readAtCloser, err error) {
|
2017-07-18 15:54:03 +00:00
|
|
|
defer func() {
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
w.fsopMu.RUnlock()
|
2017-07-18 15:54:03 +00:00
|
|
|
if err == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, f := range files {
|
|
|
|
f.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
for i := w.maxFiles; i > 1; i-- {
|
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-02-23 18:44:15 +00:00
|
|
|
var f readAtCloser
|
|
|
|
f, err = open(fmt.Sprintf("%s.%d", w.f.Name(), i-1))
|
2017-07-18 15:54:03 +00:00
|
|
|
if err != nil {
|
2022-02-15 16:31:18 +00:00
|
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
2018-05-07 15:25:41 +00:00
|
|
|
return nil, errors.Wrap(err, "error opening rotated log file")
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
2018-03-13 02:47:24 +00:00
|
|
|
|
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-02-23 18:44:15 +00:00
|
|
|
f, err = w.maybeDecompressFile(fmt.Sprintf("%s.%d.gz", w.f.Name(), i-1), config)
|
2018-03-13 02:47:24 +00:00
|
|
|
if err != nil {
|
2022-02-15 16:31:18 +00:00
|
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
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-02-23 18:44:15 +00:00
|
|
|
return nil, err
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
continue
|
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-02-23 18:44:15 +00:00
|
|
|
} else if f == nil {
|
2018-03-13 02:47:24 +00:00
|
|
|
// The log before `config.Since` does not need to read
|
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-02-23 18:44:15 +00:00
|
|
|
continue
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
files = append(files, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
return files, nil
|
|
|
|
}
|
|
|
|
|
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-02-23 18:44:15 +00:00
|
|
|
func (w *LogFile) maybeDecompressFile(fileName string, config logger.ReadConfig) (readAtCloser, error) {
|
2020-07-15 17:40:21 +00:00
|
|
|
cf, err := open(fileName)
|
2018-03-13 02:47:24 +00:00
|
|
|
if err != nil {
|
2018-05-07 15:25:41 +00:00
|
|
|
return nil, errors.Wrap(err, "error opening file for decompression")
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
defer cf.Close()
|
|
|
|
|
|
|
|
rc, err := gzip.NewReader(cf)
|
|
|
|
if err != nil {
|
2018-05-07 15:25:41 +00:00
|
|
|
return nil, errors.Wrap(err, "error making gzip reader for compressed log file")
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
defer rc.Close()
|
|
|
|
|
|
|
|
// Extract the last log entry timestramp from the gzip header
|
|
|
|
extra := &rotateFileMetadata{}
|
|
|
|
err = json.Unmarshal(rc.Header.Extra, extra)
|
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-02-23 18:44:15 +00:00
|
|
|
if err == nil && !extra.LastTime.IsZero() && extra.LastTime.Before(config.Since) {
|
2018-03-13 02:47:24 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
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-02-23 18:44:15 +00:00
|
|
|
tmpf, err := w.decompress.Do(cf)
|
|
|
|
return tmpf, errors.Wrap(err, "error decompressing log file")
|
|
|
|
}
|
2018-03-13 02:47:24 +00:00
|
|
|
|
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-02-23 18:44:15 +00:00
|
|
|
func decompress(dst io.WriteSeeker, src io.ReadSeeker) error {
|
|
|
|
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
rc, err := gzip.NewReader(src)
|
2018-03-13 02:47:24 +00:00
|
|
|
if err != nil {
|
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-02-23 18:44:15 +00:00
|
|
|
return err
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
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-02-23 18:44:15 +00:00
|
|
|
_, err = pools.Copy(dst, rc)
|
2018-03-13 02:47:24 +00:00
|
|
|
if err != nil {
|
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-02-23 18:44:15 +00:00
|
|
|
return err
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
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-02-23 18:44:15 +00:00
|
|
|
return rc.Close()
|
2018-03-13 02:47:24 +00:00
|
|
|
}
|
|
|
|
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
func tailFiles(files []SizeReaderAt, watcher *logger.LogWatcher, dec Decoder, getTailReader GetTailReaderFunc, config logger.ReadConfig) (cont bool) {
|
2018-04-05 16:39:28 +00:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
defer cancel()
|
2020-06-12 23:01:37 +00:00
|
|
|
|
|
|
|
cont = true
|
2018-04-05 16:39:28 +00:00
|
|
|
// TODO(@cpuguy83): we should plumb a context through instead of dealing with `WatchClose()` here.
|
|
|
|
go func() {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
daemon.ContainerLogs(): fix resource leak on follow
When daemon.ContainerLogs() is called with options.follow=true
(as in "docker logs --follow"), the "loggerutils.followLogs()"
function never returns (even then the logs consumer is gone).
As a result, all the resources associated with it (including
an opened file descriptor for the log file being read, two FDs
for a pipe, and two FDs for inotify watch) are never released.
If this is repeated (such as by running "docker logs --follow"
and pressing Ctrl-C a few times), this results in DoS caused by
either hitting the limit of inotify watches, or the limit of
opened files. The only cure is daemon restart.
Apparently, what happens is:
1. logs producer (a container) is gone, calling (*LogWatcher).Close()
for all its readers (daemon/logger/jsonfilelog/jsonfilelog.go:175).
2. WatchClose() is properly handled by a dedicated goroutine in
followLogs(), cancelling the context.
3. Upon receiving the ctx.Done(), the code in followLogs()
(daemon/logger/loggerutils/logfile.go#L626-L638) keeps to
send messages _synchronously_ (which is OK for now).
4. Logs consumer is gone (Ctrl-C is pressed on a terminal running
"docker logs --follow"). Method (*LogWatcher).Close() is properly
called (see daemon/logs.go:114). Since it was called before and
due to to once.Do(), nothing happens (which is kinda good, as
otherwise it will panic on closing a closed channel).
5. A goroutine (see item 3 above) keeps sending log messages
synchronously to the logWatcher.Msg channel. Since the
channel reader is gone, the channel send operation blocks forever,
and resource cleanup set up in defer statements at the beginning
of followLogs() never happens.
Alas, the fix is somewhat complicated:
1. Distinguish between close from logs producer and logs consumer.
To that effect,
- yet another channel is added to LogWatcher();
- {Watch,}Close() are renamed to {Watch,}ProducerGone();
- {Watch,}ConsumerGone() are added;
*NOTE* that ProducerGone()/WatchProducerGone() pair is ONLY needed
in order to stop ConsumerLogs(follow=true) when a container is stopped;
otherwise we're not interested in it. In other words, we're only
using it in followLogs().
2. Code that was doing (logWatcher*).Close() is modified to either call
ProducerGone() or ConsumerGone(), depending on the context.
3. Code that was waiting for WatchClose() is modified to wait for
either ConsumerGone() or ProducerGone(), or both, depending on the
context.
4. followLogs() are modified accordingly:
- context cancellation is happening on WatchProducerGone(),
and once it's received the FileWatcher is closed and waitRead()
returns errDone on EOF (i.e. log rotation handling logic is disabled);
- due to this, code that was writing synchronously to logWatcher.Msg
can be and is removed as the code above it handles this case;
- function returns once ConsumerGone is received, freeing all the
resources -- this is the bugfix itself.
While at it,
1. Let's also remove the ctx usage to simplify the code a bit.
It was introduced by commit a69a59ffc7e3d ("Decouple removing the
fileWatcher from reading") in order to fix a bug. The bug was actually
a deadlock in fsnotify, and the fix was just a workaround. Since then
the fsnofify bug has been fixed, and a new fsnotify was vendored in.
For more details, please see
https://github.com/moby/moby/pull/27782#issuecomment-416794490
2. Since `(*filePoller).Close()` is fixed to remove all the files
being watched, there is no need to explicitly call
fileWatcher.Remove(name) anymore, so get rid of the extra code.
Should fix https://github.com/moby/moby/issues/37391
Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2018-08-01 04:03:55 +00:00
|
|
|
case <-watcher.WatchConsumerGone():
|
2020-06-12 23:01:37 +00:00
|
|
|
cont = false
|
2018-04-05 16:39:28 +00:00
|
|
|
cancel()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
readers := make([]io.Reader, 0, len(files))
|
2017-07-18 15:54:03 +00:00
|
|
|
|
|
|
|
if config.Tail > 0 {
|
daemon/logger: follow LogFile without file watches
File watches have been a source of complexity and unreliability in the
LogFile follow implementation, especially when combined with file
rotation. File change events can be unreliably delivered, especially on
Windows, and the polling fallback adds latency. Following across
rotations has never worked reliably on Windows. Without synchronization
between the log writer and readers, race conditions abound: readers can
read from the file while a log entry is only partially written, leading
to decode errors and necessitating retries.
In addition to the complexities stemming from file watches, the LogFile
follow implementation had complexity from needing to handle file
truncations, and (due to a now-fixed bug in the polling file watcher
implementation) evictions to unlock the log file so it could be rotated.
Log files are now always rotated, never truncated, so these situations
no longer need to be handled by the follow code.
Rewrite the LogFile follow implementation in terms of waiting until
LogFile notifies it that a new message has been written to the log file.
The LogFile informs the follower of the file offset of the last complete
write so that the follower knows not to read past that, preventing it
from attempting to decode partial messages and making retries
unnecessary. Synchronization between LogFile and its followers is used
at critical points to prevent missed notifications of writes and races
between file rotations and the follower opening files for read.
Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-02-09 19:49:23 +00:00
|
|
|
nLines := config.Tail
|
2018-04-05 16:39:28 +00:00
|
|
|
for i := len(files) - 1; i >= 0 && nLines > 0; i-- {
|
|
|
|
tail, n, err := getTailReader(ctx, files[i], nLines)
|
|
|
|
if err != nil {
|
|
|
|
watcher.Err <- errors.Wrap(err, "error finding file position to start log tailing")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
nLines -= n
|
|
|
|
readers = append([]io.Reader{tail}, readers...)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for _, r := range files {
|
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-02-23 18:44:15 +00:00
|
|
|
readers = append(readers, r)
|
2017-07-18 15:54:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-05 16:39:28 +00:00
|
|
|
rdr := io.MultiReader(readers...)
|
2020-04-08 19:24:31 +00:00
|
|
|
dec.Reset(rdr)
|
|
|
|
|
2017-07-18 15:54:03 +00:00
|
|
|
for {
|
2020-04-08 19:24:31 +00:00
|
|
|
msg, err := dec.Decode()
|
2017-07-18 15:54:03 +00:00
|
|
|
if err != nil {
|
2020-04-17 10:01:01 +00:00
|
|
|
if !errors.Is(err, io.EOF) {
|
2017-07-18 15:54:03 +00:00
|
|
|
watcher.Err <- err
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !config.Since.IsZero() && msg.Timestamp.Before(config.Since) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !config.Until.IsZero() && msg.Timestamp.After(config.Until) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
select {
|
2018-04-05 16:39:28 +00:00
|
|
|
case <-ctx.Done():
|
2017-07-18 15:54:03 +00:00
|
|
|
return
|
|
|
|
case watcher.Msg <- msg:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|