feat(inputs.docker_log): Add state-persistence capabilities (#12775)

This commit is contained in:
Sven Rebhan 2023-03-06 12:33:23 +01:00 committed by GitHub
parent 360edd52b6
commit 119a95dc72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 20 deletions

View File

@ -31,8 +31,9 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## To use environment variables (ie, docker-machine), set endpoint = "ENV" ## To use environment variables (ie, docker-machine), set endpoint = "ENV"
# endpoint = "unix:///var/run/docker.sock" # endpoint = "unix:///var/run/docker.sock"
## When true, container logs are read from the beginning; otherwise ## When true, container logs are read from the beginning; otherwise reading
## reading begins at the end of the log. ## begins at the end of the log. If state-persistence is enabled for Telegraf,
## the reading continues at the last previously processed timestamp.
# from_beginning = false # from_beginning = false
## Timeout for Docker API calls. ## Timeout for Docker API calls.

View File

@ -65,6 +65,11 @@ type DockerLogs struct {
wg sync.WaitGroup wg sync.WaitGroup
mu sync.Mutex mu sync.Mutex
containerList map[string]context.CancelFunc containerList map[string]context.CancelFunc
// State of the plugin mapping container-ID to the timestamp of the
// last record processed
lastRecord map[string]time.Time
lastRecordMtx sync.Mutex
} }
func (*DockerLogs) SampleConfig() string { func (*DockerLogs) SampleConfig() string {
@ -116,6 +121,34 @@ func (d *DockerLogs) Init() error {
} }
} }
d.lastRecord = make(map[string]time.Time)
return nil
}
// State persistence interfaces
func (d *DockerLogs) GetState() interface{} {
d.lastRecordMtx.Lock()
recordOffsets := make(map[string]time.Time, len(d.lastRecord))
for k, v := range d.lastRecord {
recordOffsets[k] = v
}
d.lastRecordMtx.Unlock()
return recordOffsets
}
func (d *DockerLogs) SetState(state interface{}) error {
recordOffsets, ok := state.(map[string]time.Time)
if !ok {
return fmt.Errorf("state has wrong type %T", state)
}
d.lastRecordMtx.Lock()
for k, v := range recordOffsets {
d.lastRecord[k] = v
}
d.lastRecordMtx.Unlock()
return nil return nil
} }
@ -237,9 +270,13 @@ func (d *DockerLogs) tailContainerLogs(
return err return err
} }
tail := "0" since := time.Time{}.Format(time.RFC3339Nano)
if d.FromBeginning { if !d.FromBeginning {
tail = "all" d.lastRecordMtx.Lock()
if ts, ok := d.lastRecord[container.ID]; ok {
since = ts.Format(time.RFC3339Nano)
}
d.lastRecordMtx.Unlock()
} }
logOptions := types.ContainerLogsOptions{ logOptions := types.ContainerLogsOptions{
@ -248,7 +285,7 @@ func (d *DockerLogs) tailContainerLogs(
Timestamps: true, Timestamps: true,
Details: false, Details: false,
Follow: true, Follow: true,
Tail: tail, Since: since,
} }
logReader, err := d.client.ContainerLogs(ctx, container.ID, logOptions) logReader, err := d.client.ContainerLogs(ctx, container.ID, logOptions)
@ -262,10 +299,23 @@ func (d *DockerLogs) tailContainerLogs(
// //
// If the container is *not* using a TTY, streams for stdout and stderr are // If the container is *not* using a TTY, streams for stdout and stderr are
// multiplexed. // multiplexed.
var last time.Time
if hasTTY { if hasTTY {
return tailStream(acc, tags, container.ID, logReader, "tty") last, err = tailStream(acc, tags, container.ID, logReader, "tty")
} else {
last, err = tailMultiplexed(acc, tags, container.ID, logReader)
} }
return tailMultiplexed(acc, tags, container.ID, logReader) if err != nil {
return err
}
if ts, ok := d.lastRecord[container.ID]; !ok || ts.Before(last) {
d.lastRecordMtx.Lock()
d.lastRecord[container.ID] = last
d.lastRecordMtx.Unlock()
}
return nil
} }
func parseLine(line []byte) (time.Time, string, error) { func parseLine(line []byte) (time.Time, string, error) {
@ -297,7 +347,7 @@ func tailStream(
containerID string, containerID string,
reader io.ReadCloser, reader io.ReadCloser,
stream string, stream string,
) error { ) (time.Time, error) {
defer reader.Close() defer reader.Close()
tags := make(map[string]string, len(baseTags)+1) tags := make(map[string]string, len(baseTags)+1)
@ -308,6 +358,7 @@ func tailStream(
r := bufio.NewReaderSize(reader, 64*1024) r := bufio.NewReaderSize(reader, 64*1024)
var lastTs time.Time
for { for {
line, err := r.ReadBytes('\n') line, err := r.ReadBytes('\n')
@ -321,13 +372,18 @@ func tailStream(
"message": message, "message": message,
}, tags, ts) }, tags, ts)
} }
// Store the last processed timestamp
if ts.After(lastTs) {
lastTs = ts
}
} }
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
return nil return lastTs, nil
} }
return err return time.Time{}, err
} }
} }
} }
@ -337,15 +393,17 @@ func tailMultiplexed(
tags map[string]string, tags map[string]string,
containerID string, containerID string,
src io.ReadCloser, src io.ReadCloser,
) error { ) (time.Time, error) {
outReader, outWriter := io.Pipe() outReader, outWriter := io.Pipe()
errReader, errWriter := io.Pipe() errReader, errWriter := io.Pipe()
var tsStdout, tsStderr time.Time
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
err := tailStream(acc, tags, containerID, outReader, "stdout") var err error
tsStdout, err = tailStream(acc, tags, containerID, outReader, "stdout")
if err != nil { if err != nil {
acc.AddError(err) acc.AddError(err)
} }
@ -354,18 +412,28 @@ func tailMultiplexed(
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
err := tailStream(acc, tags, containerID, errReader, "stderr") var err error
tsStderr, err = tailStream(acc, tags, containerID, errReader, "stderr")
if err != nil { if err != nil {
acc.AddError(err) acc.AddError(err)
} }
}() }()
_, err := stdcopy.StdCopy(outWriter, errWriter, src) _, err := stdcopy.StdCopy(outWriter, errWriter, src)
outWriter.Close() //nolint:revive // we cannot do anything if the closing fails
errWriter.Close() //nolint:revive // we cannot do anything if the closing fails // Ignore the returned errors as we cannot do anything if the closing fails
src.Close() //nolint:revive // we cannot do anything if the closing fails _ = outWriter.Close()
_ = errWriter.Close()
_ = src.Close()
wg.Wait() wg.Wait()
return err
if err != nil {
return time.Time{}, err
}
if tsStdout.After(tsStderr) {
return tsStdout, nil
}
return tsStderr, nil
} }
// Start is a noop which is required for a *DockerLogs to implement // Start is a noop which is required for a *DockerLogs to implement

View File

@ -5,8 +5,9 @@
## To use environment variables (ie, docker-machine), set endpoint = "ENV" ## To use environment variables (ie, docker-machine), set endpoint = "ENV"
# endpoint = "unix:///var/run/docker.sock" # endpoint = "unix:///var/run/docker.sock"
## When true, container logs are read from the beginning; otherwise ## When true, container logs are read from the beginning; otherwise reading
## reading begins at the end of the log. ## begins at the end of the log. If state-persistence is enabled for Telegraf,
## the reading continues at the last previously processed timestamp.
# from_beginning = false # from_beginning = false
## Timeout for Docker API calls. ## Timeout for Docker API calls.