Files
docker-cli/components/engine/daemon/logger/jsonfilelog/jsonfilelog.go
T
Shijiang Wei 3e20fd363b fix a race in json logger reader
The json decoder starts to decode immediately an inotify event is
received.
But at the time the inotify event is trigged, the json log
entry might haven't been fully written to the disk.
In this case the decoder will return an "io.UnexpectedEOF" error, but
there is still data remaining in the decoder's buffer. And the data
should be passed to the decoder when the next inotify event is
triggered.

Signed-off-by: Shijiang Wei <mountkin@gmail.com>
Upstream-commit: e41eae8b42052d07432405cca113ffcf8cf49c06
Component: engine
2015-10-31 02:28:23 +08:00

421 lines
9.4 KiB
Go

// Package jsonfilelog provides the default Logger implementation for
// Docker logging. This logger logs to files on the host server in the
// JSON format.
package jsonfilelog
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"sync"
"time"
"gopkg.in/fsnotify.v1"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/daemon/logger"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/jsonlog"
"github.com/docker/docker/pkg/pubsub"
"github.com/docker/docker/pkg/tailfile"
"github.com/docker/docker/pkg/timeutils"
"github.com/docker/docker/pkg/units"
)
const (
// Name is the name of the file that the jsonlogger logs to.
Name = "json-file"
maxJSONDecodeRetry = 10
)
// JSONFileLogger is Logger implementation for default Docker logging.
type JSONFileLogger struct {
buf *bytes.Buffer
f *os.File // store for closing
mu sync.Mutex // protects buffer
capacity int64 //maximum size of each file
n int //maximum number of files
ctx logger.Context
readers map[*logger.LogWatcher]struct{} // stores the active log followers
notifyRotate *pubsub.Publisher
extra []byte // json-encoded extra attributes
}
func init() {
if err := logger.RegisterLogDriver(Name, New); err != nil {
logrus.Fatal(err)
}
if err := logger.RegisterLogOptValidator(Name, ValidateLogOpt); err != nil {
logrus.Fatal(err)
}
}
// New creates new JSONFileLogger which writes to filename passed in
// on given context.
func New(ctx logger.Context) (logger.Logger, error) {
log, err := os.OpenFile(ctx.LogPath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
var capval int64 = -1
if capacity, ok := ctx.Config["max-size"]; ok {
var err error
capval, err = units.FromHumanSize(capacity)
if err != nil {
return nil, err
}
}
var maxFiles = 1
if maxFileString, ok := ctx.Config["max-file"]; ok {
maxFiles, err = strconv.Atoi(maxFileString)
if err != nil {
return nil, err
}
if maxFiles < 1 {
return nil, fmt.Errorf("max-file cannot be less than 1")
}
}
var extra []byte
if attrs := ctx.ExtraAttributes(nil); len(attrs) > 0 {
var err error
extra, err = json.Marshal(attrs)
if err != nil {
return nil, err
}
}
return &JSONFileLogger{
f: log,
buf: bytes.NewBuffer(nil),
ctx: ctx,
capacity: capval,
n: maxFiles,
readers: make(map[*logger.LogWatcher]struct{}),
notifyRotate: pubsub.NewPublisher(0, 1),
extra: extra,
}, nil
}
// Log converts logger.Message to jsonlog.JSONLog and serializes it to file.
func (l *JSONFileLogger) Log(msg *logger.Message) error {
l.mu.Lock()
defer l.mu.Unlock()
timestamp, err := timeutils.FastMarshalJSON(msg.Timestamp)
if err != nil {
return err
}
err = (&jsonlog.JSONLogs{
Log: append(msg.Line, '\n'),
Stream: msg.Source,
Created: timestamp,
RawAttrs: l.extra,
}).MarshalJSONBuf(l.buf)
if err != nil {
return err
}
l.buf.WriteByte('\n')
_, err = writeLog(l)
return err
}
func writeLog(l *JSONFileLogger) (int64, error) {
if l.capacity == -1 {
return writeToBuf(l)
}
meta, err := l.f.Stat()
if err != nil {
return -1, err
}
if meta.Size() >= l.capacity {
name := l.f.Name()
if err := l.f.Close(); err != nil {
return -1, err
}
if err := rotate(name, l.n); err != nil {
return -1, err
}
file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
return -1, err
}
l.f = file
l.notifyRotate.Publish(struct{}{})
}
return writeToBuf(l)
}
func writeToBuf(l *JSONFileLogger) (int64, error) {
i, err := l.buf.WriteTo(l.f)
if err != nil {
l.buf = bytes.NewBuffer(nil)
}
return i, err
}
func rotate(name string, n int) error {
if n < 2 {
return nil
}
for i := n - 1; i > 1; i-- {
oldFile := name + "." + strconv.Itoa(i)
replacingFile := name + "." + strconv.Itoa(i-1)
if err := backup(oldFile, replacingFile); err != nil {
return err
}
}
if err := backup(name+".1", name); err != nil {
return err
}
return nil
}
// backup renames a file from curr to old, creating an empty file curr if it does not exist.
func backup(old, curr string) error {
if _, err := os.Stat(old); !os.IsNotExist(err) {
err := os.Remove(old)
if err != nil {
return err
}
}
if _, err := os.Stat(curr); os.IsNotExist(err) {
f, err := os.Create(curr)
if err != nil {
return err
}
f.Close()
}
return os.Rename(curr, old)
}
// ValidateLogOpt looks for json specific log options max-file & max-size.
func ValidateLogOpt(cfg map[string]string) error {
for key := range cfg {
switch key {
case "max-file":
case "max-size":
case "labels":
case "env":
default:
return fmt.Errorf("unknown log opt '%s' for json-file log driver", key)
}
}
return nil
}
// LogPath returns the location the given json logger logs to.
func (l *JSONFileLogger) LogPath() string {
return l.ctx.LogPath
}
// Close closes underlying file and signals all readers to stop.
func (l *JSONFileLogger) Close() error {
l.mu.Lock()
err := l.f.Close()
for r := range l.readers {
r.Close()
delete(l.readers, r)
}
l.mu.Unlock()
return err
}
// Name returns name of this logger.
func (l *JSONFileLogger) Name() string {
return Name
}
func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, error) {
l.Reset()
if err := dec.Decode(l); err != nil {
return nil, err
}
msg := &logger.Message{
Source: l.Stream,
Timestamp: l.Created,
Line: []byte(l.Log),
}
return msg, nil
}
// ReadLogs implements the logger's LogReader interface for the logs
// created by this driver.
func (l *JSONFileLogger) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
logWatcher := logger.NewLogWatcher()
go l.readLogs(logWatcher, config)
return logWatcher
}
func (l *JSONFileLogger) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) {
defer close(logWatcher.Msg)
pth := l.ctx.LogPath
var files []io.ReadSeeker
for i := l.n; i > 1; i-- {
f, err := os.Open(fmt.Sprintf("%s.%d", pth, i-1))
if err != nil {
if !os.IsNotExist(err) {
logWatcher.Err <- err
break
}
continue
}
defer f.Close()
files = append(files, f)
}
latestFile, err := os.Open(pth)
if err != nil {
logWatcher.Err <- err
return
}
defer latestFile.Close()
files = append(files, latestFile)
tailer := ioutils.MultiReadSeeker(files...)
if config.Tail != 0 {
tailFile(tailer, logWatcher, config.Tail, config.Since)
}
if !config.Follow {
return
}
if config.Tail >= 0 {
latestFile.Seek(0, os.SEEK_END)
}
l.mu.Lock()
l.readers[logWatcher] = struct{}{}
l.mu.Unlock()
notifyRotate := l.notifyRotate.Subscribe()
followLogs(latestFile, logWatcher, notifyRotate, config.Since)
l.mu.Lock()
delete(l.readers, logWatcher)
l.mu.Unlock()
l.notifyRotate.Evict(notifyRotate)
}
func tailFile(f io.ReadSeeker, logWatcher *logger.LogWatcher, tail int, since time.Time) {
var rdr io.Reader = f
if tail > 0 {
ls, err := tailfile.TailFile(f, tail)
if err != nil {
logWatcher.Err <- err
return
}
rdr = bytes.NewBuffer(bytes.Join(ls, []byte("\n")))
}
dec := json.NewDecoder(rdr)
l := &jsonlog.JSONLog{}
for {
msg, err := decodeLogLine(dec, l)
if err != nil {
if err != io.EOF {
logWatcher.Err <- err
}
return
}
if !since.IsZero() && msg.Timestamp.Before(since) {
continue
}
logWatcher.Msg <- msg
}
}
func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan interface{}, since time.Time) {
dec := json.NewDecoder(f)
l := &jsonlog.JSONLog{}
fileWatcher, err := fsnotify.NewWatcher()
if err != nil {
logWatcher.Err <- err
return
}
defer fileWatcher.Close()
if err := fileWatcher.Add(f.Name()); err != nil {
logWatcher.Err <- err
return
}
var retries int
for {
msg, err := decodeLogLine(dec, l)
if err != nil {
if err != io.EOF {
// try again because this shouldn't happen
if _, ok := err.(*json.SyntaxError); ok && retries <= maxJSONDecodeRetry {
dec = json.NewDecoder(f)
retries++
continue
}
// io.ErrUnexpectedEOF is returned from json.Decoder when there is
// remaining data in the parser's buffer while an io.EOF occurs.
// If the json logger writes a partial json log entry to the disk
// while at the same time the decoder tries to decode it, the race codition happens.
if err == io.ErrUnexpectedEOF && retries <= maxJSONDecodeRetry {
reader := io.MultiReader(dec.Buffered(), f)
dec = json.NewDecoder(reader)
retries++
continue
}
logWatcher.Err <- err
return
}
select {
case <-fileWatcher.Events:
dec = json.NewDecoder(f)
continue
case <-fileWatcher.Errors:
logWatcher.Err <- err
return
case <-logWatcher.WatchClose():
return
case <-notifyRotate:
fileWatcher.Remove(f.Name())
f, err = os.Open(f.Name())
if err != nil {
logWatcher.Err <- err
return
}
if err := fileWatcher.Add(f.Name()); err != nil {
logWatcher.Err <- err
}
dec = json.NewDecoder(f)
continue
}
}
retries = 0 // reset retries since we've succeeded
if !since.IsZero() && msg.Timestamp.Before(since) {
continue
}
select {
case logWatcher.Msg <- msg:
case <-logWatcher.WatchClose():
logWatcher.Msg <- msg
for {
msg, err := decodeLogLine(dec, l)
if err != nil {
return
}
if !since.IsZero() && msg.Timestamp.Before(since) {
continue
}
logWatcher.Msg <- msg
}
}
}
}