Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
114
plugins/inputs/docker_log/README.md
Normal file
114
plugins/inputs/docker_log/README.md
Normal file
|
@ -0,0 +1,114 @@
|
|||
# Docker Log Input Plugin
|
||||
|
||||
This plugin uses the [Docker Engine API][api] to gather logs from running
|
||||
docker containers.
|
||||
|
||||
> [!NOTE]
|
||||
> This plugin works only for containers with the `local` or `json-file` or
|
||||
> `journald` logging driver. Please make sure Telegraf has sufficient
|
||||
> permissions to access the configured endpoint!
|
||||
|
||||
⭐ Telegraf v1.12.0
|
||||
🏷️ containers, logging
|
||||
💻 all
|
||||
|
||||
[api]: https://docs.docker.com/engine/api
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Read logging output from the Docker engine
|
||||
[[inputs.docker_log]]
|
||||
## Docker Endpoint
|
||||
## To use TCP, set endpoint = "tcp://[ip]:[port]"
|
||||
## To use environment variables (ie, docker-machine), set endpoint = "ENV"
|
||||
# endpoint = "unix:///var/run/docker.sock"
|
||||
|
||||
## When true, container logs are read from the beginning; otherwise reading
|
||||
## 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
|
||||
|
||||
## Timeout for Docker API calls.
|
||||
# timeout = "5s"
|
||||
|
||||
## Containers to include and exclude. Globs accepted.
|
||||
## Note that an empty array for both will include all containers
|
||||
# container_name_include = []
|
||||
# container_name_exclude = []
|
||||
|
||||
## Container states to include and exclude. Globs accepted.
|
||||
## When empty only containers in the "running" state will be captured.
|
||||
# container_state_include = []
|
||||
# container_state_exclude = []
|
||||
|
||||
## docker labels to include and exclude as tags. Globs accepted.
|
||||
## Note that an empty array for both will include all labels as tags
|
||||
# docker_label_include = []
|
||||
# docker_label_exclude = []
|
||||
|
||||
## Set the source tag for the metrics to the container ID hostname, eg first 12 chars
|
||||
source_tag = false
|
||||
|
||||
## Optional TLS Config
|
||||
# tls_ca = "/etc/telegraf/ca.pem"
|
||||
# tls_cert = "/etc/telegraf/cert.pem"
|
||||
# tls_key = "/etc/telegraf/key.pem"
|
||||
## Use TLS but skip chain & host verification
|
||||
# insecure_skip_verify = false
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
When using the `"ENV"` endpoint, the connection is configured using the
|
||||
[CLI Docker environment variables][env]
|
||||
|
||||
[env]: https://godoc.org/github.com/moby/moby/client#NewEnvClient
|
||||
|
||||
## source tag
|
||||
|
||||
Selecting the containers can be tricky if you have many containers with the same
|
||||
name. To alleviate this issue you can set the below value to `true`
|
||||
|
||||
```toml
|
||||
source_tag = true
|
||||
```
|
||||
|
||||
This will cause all data points to have the `source` tag be set to the first 12
|
||||
characters of the container id. The first 12 characters is the common hostname
|
||||
for containers that have no explicit hostname set, as defined by docker.
|
||||
|
||||
## Metrics
|
||||
|
||||
- docker_log
|
||||
- tags:
|
||||
- container_image
|
||||
- container_version
|
||||
- container_name
|
||||
- stream (stdout, stderr, or tty)
|
||||
- source
|
||||
- fields:
|
||||
- container_id
|
||||
- message
|
||||
|
||||
## Example Output
|
||||
|
||||
```text
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! [agent] Config: Interval:10s, Quiet:false, Hostname:\"371ee5d3e587\", Flush Interval:10s" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Tags enabled: host=371ee5d3e587" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Loaded outputs: file" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Loaded processors:" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Loaded aggregators:" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Loaded inputs: net" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Using config file: /etc/telegraf/telegraf.conf" 1560913872000000000
|
||||
docker_log,container_image=telegraf,container_name=sharp_bell,container_version=alpine,stream=stderr container_id="371ee5d3e58726112f499be62cddef800138ca72bbba635ed2015fbf475b1023",message="2019-06-19T03:11:11Z I! Starting Telegraf 1.10.4" 1560913872000000000
|
||||
```
|
70
plugins/inputs/docker_log/client.go
Normal file
70
plugins/inputs/docker_log/client.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package docker_log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
docker "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// This file is inherited from telegraf docker input plugin
|
||||
var (
|
||||
version = "1.24"
|
||||
defaultHeaders = map[string]string{"User-Agent": "engine-api-cli-1.0"}
|
||||
)
|
||||
|
||||
type dockerClient interface {
|
||||
// ContainerList lists the containers in the Docker environment.
|
||||
ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error)
|
||||
// ContainerLogs retrieves the logs of a specific container.
|
||||
ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error)
|
||||
// ContainerInspect inspects a specific container and retrieves its details.
|
||||
ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error)
|
||||
}
|
||||
|
||||
func newEnvClient() (dockerClient, error) {
|
||||
client, err := docker.NewClientWithOpts(docker.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &socketClient{client}, nil
|
||||
}
|
||||
|
||||
func newClient(host string, tlsConfig *tls.Config) (dockerClient, error) {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
httpClient := &http.Client{Transport: transport}
|
||||
client, err := docker.NewClientWithOpts(
|
||||
docker.WithHTTPHeaders(defaultHeaders),
|
||||
docker.WithHTTPClient(httpClient),
|
||||
docker.WithVersion(version),
|
||||
docker.WithHost(host))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &socketClient{client}, nil
|
||||
}
|
||||
|
||||
type socketClient struct {
|
||||
client *docker.Client
|
||||
}
|
||||
|
||||
// ContainerList lists the containers in the Docker environment.
|
||||
func (c *socketClient) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
return c.client.ContainerList(ctx, options)
|
||||
}
|
||||
|
||||
// ContainerLogs retrieves the logs of a specific container.
|
||||
func (c *socketClient) ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
return c.client.ContainerLogs(ctx, containerID, options)
|
||||
}
|
||||
|
||||
// ContainerInspect inspects a specific container and retrieves its details.
|
||||
func (c *socketClient) ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) {
|
||||
return c.client.ContainerInspect(ctx, containerID)
|
||||
}
|
498
plugins/inputs/docker_log/docker_log.go
Normal file
498
plugins/inputs/docker_log/docker_log.go
Normal file
|
@ -0,0 +1,498 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package docker_log
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/filter"
|
||||
"github.com/influxdata/telegraf/internal/docker"
|
||||
common_tls "github.com/influxdata/telegraf/plugins/common/tls"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
var (
|
||||
// ensure *DockerLogs implements telegraf.ServiceInput
|
||||
_ telegraf.ServiceInput = (*DockerLogs)(nil)
|
||||
containerStates = []string{"created", "restarting", "running", "removing", "paused", "exited", "dead"}
|
||||
)
|
||||
|
||||
const (
|
||||
defaultEndpoint = "unix:///var/run/docker.sock"
|
||||
)
|
||||
|
||||
type DockerLogs struct {
|
||||
Endpoint string `toml:"endpoint"`
|
||||
FromBeginning bool `toml:"from_beginning"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
LabelInclude []string `toml:"docker_label_include"`
|
||||
LabelExclude []string `toml:"docker_label_exclude"`
|
||||
ContainerInclude []string `toml:"container_name_include"`
|
||||
ContainerExclude []string `toml:"container_name_exclude"`
|
||||
ContainerStateInclude []string `toml:"container_state_include"`
|
||||
ContainerStateExclude []string `toml:"container_state_exclude"`
|
||||
IncludeSourceTag bool `toml:"source_tag"`
|
||||
|
||||
common_tls.ClientConfig
|
||||
|
||||
newEnvClient func() (dockerClient, error)
|
||||
newClient func(string, *tls.Config) (dockerClient, error)
|
||||
|
||||
client dockerClient
|
||||
labelFilter filter.Filter
|
||||
containerFilter filter.Filter
|
||||
stateFilter filter.Filter
|
||||
opts container.ListOptions
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
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 {
|
||||
return sampleConfig
|
||||
}
|
||||
|
||||
func (d *DockerLogs) Init() error {
|
||||
var err error
|
||||
if d.Endpoint == "ENV" {
|
||||
d.client, err = d.newEnvClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
tlsConfig, err := d.ClientConfig.TLSConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.client, err = d.newClient(d.Endpoint, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create filters
|
||||
err = d.createLabelFilters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.createContainerFilters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.createContainerStateFilters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filterArgs := filters.NewArgs()
|
||||
for _, state := range containerStates {
|
||||
if d.stateFilter.Match(state) {
|
||||
filterArgs.Add("status", state)
|
||||
}
|
||||
}
|
||||
|
||||
if filterArgs.Len() != 0 {
|
||||
d.opts = container.ListOptions{
|
||||
Filters: filterArgs,
|
||||
}
|
||||
}
|
||||
|
||||
d.lastRecord = make(map[string]time.Time)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start is a noop which is required for a *DockerLogs to implement the telegraf.ServiceInput interface
|
||||
func (*DockerLogs) Start(telegraf.Accumulator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (d *DockerLogs) Gather(acc telegraf.Accumulator) error {
|
||||
ctx := context.Background()
|
||||
acc.SetPrecision(time.Nanosecond)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(d.Timeout))
|
||||
defer cancel()
|
||||
containers, err := d.client.ContainerList(ctx, d.opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cntnr := range containers {
|
||||
if d.containerInContainerList(cntnr.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
containerName := d.matchedContainerName(cntnr.Names)
|
||||
if containerName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
d.addToContainerList(cntnr.ID, cancel)
|
||||
|
||||
// Start a new goroutine for every new container that has logs to collect
|
||||
d.wg.Add(1)
|
||||
go func(container container.Summary) {
|
||||
defer d.wg.Done()
|
||||
defer d.removeFromContainerList(container.ID)
|
||||
|
||||
err = d.tailContainerLogs(ctx, acc, container, containerName)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
acc.AddError(err)
|
||||
}
|
||||
}(cntnr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerLogs) Stop() {
|
||||
d.cancelTails()
|
||||
d.wg.Wait()
|
||||
}
|
||||
|
||||
func (d *DockerLogs) addToContainerList(containerID string, cancel context.CancelFunc) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.containerList[containerID] = cancel
|
||||
}
|
||||
|
||||
func (d *DockerLogs) removeFromContainerList(containerID string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
delete(d.containerList, containerID)
|
||||
}
|
||||
|
||||
func (d *DockerLogs) containerInContainerList(containerID string) bool {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
_, ok := d.containerList[containerID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *DockerLogs) cancelTails() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for _, cancel := range d.containerList {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DockerLogs) matchedContainerName(names []string) string {
|
||||
// Check if all container names are filtered; in practice I believe
|
||||
// this array is always of length 1.
|
||||
for _, name := range names {
|
||||
trimmedName := strings.TrimPrefix(name, "/")
|
||||
if !strings.Contains(trimmedName, "/") {
|
||||
match := d.containerFilter.Match(trimmedName)
|
||||
if match {
|
||||
return trimmedName
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *DockerLogs) hasTTY(ctx context.Context, cntnr container.Summary) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(d.Timeout))
|
||||
defer cancel()
|
||||
c, err := d.client.ContainerInspect(ctx, cntnr.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return c.Config.Tty, nil
|
||||
}
|
||||
|
||||
func (d *DockerLogs) tailContainerLogs(
|
||||
ctx context.Context,
|
||||
acc telegraf.Accumulator,
|
||||
cntnr container.Summary,
|
||||
containerName string,
|
||||
) error {
|
||||
imageName, imageVersion := docker.ParseImage(cntnr.Image)
|
||||
tags := map[string]string{
|
||||
"container_name": containerName,
|
||||
"container_image": imageName,
|
||||
"container_version": imageVersion,
|
||||
}
|
||||
|
||||
if d.IncludeSourceTag {
|
||||
tags["source"] = hostnameFromID(cntnr.ID)
|
||||
}
|
||||
|
||||
// Add matching container labels as tags
|
||||
for k, label := range cntnr.Labels {
|
||||
if d.labelFilter.Match(k) {
|
||||
tags[k] = label
|
||||
}
|
||||
}
|
||||
|
||||
hasTTY, err := d.hasTTY(ctx, cntnr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
since := time.Time{}.Format(time.RFC3339Nano)
|
||||
if !d.FromBeginning {
|
||||
d.lastRecordMtx.Lock()
|
||||
if ts, ok := d.lastRecord[cntnr.ID]; ok {
|
||||
since = ts.Format(time.RFC3339Nano)
|
||||
}
|
||||
d.lastRecordMtx.Unlock()
|
||||
}
|
||||
|
||||
logOptions := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Timestamps: true,
|
||||
Details: false,
|
||||
Follow: true,
|
||||
Since: since,
|
||||
}
|
||||
|
||||
logReader, err := d.client.ContainerLogs(ctx, cntnr.ID, logOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the container is using a TTY, there is only a single stream
|
||||
// (stdout), and data is copied directly from the container output stream,
|
||||
// no extra multiplexing or headers.
|
||||
//
|
||||
// If the container is *not* using a TTY, streams for stdout and stderr are
|
||||
// multiplexed.
|
||||
var last time.Time
|
||||
if hasTTY {
|
||||
last, err = tailStream(acc, tags, cntnr.ID, logReader, "tty")
|
||||
} else {
|
||||
last, err = tailMultiplexed(acc, tags, cntnr.ID, logReader)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ts, ok := d.lastRecord[cntnr.ID]; !ok || ts.Before(last) {
|
||||
d.lastRecordMtx.Lock()
|
||||
d.lastRecord[cntnr.ID] = last
|
||||
d.lastRecordMtx.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseLine(line []byte) (time.Time, string, error) {
|
||||
parts := bytes.SplitN(line, []byte(" "), 2)
|
||||
|
||||
if len(parts) == 1 {
|
||||
parts = append(parts, []byte(""))
|
||||
}
|
||||
|
||||
tsString := string(parts[0])
|
||||
|
||||
// Keep any leading space, but remove whitespace from end of line.
|
||||
// This preserves space in, for example, stacktraces, while removing
|
||||
// annoying end of line characters and is similar to how other logging
|
||||
// plugins such as syslog behave.
|
||||
message := bytes.TrimRightFunc(parts[1], unicode.IsSpace)
|
||||
|
||||
ts, err := time.Parse(time.RFC3339Nano, tsString)
|
||||
if err != nil {
|
||||
return time.Time{}, "", fmt.Errorf("error parsing timestamp %q: %w", tsString, err)
|
||||
}
|
||||
|
||||
return ts, string(message), nil
|
||||
}
|
||||
|
||||
func tailStream(
|
||||
acc telegraf.Accumulator,
|
||||
baseTags map[string]string,
|
||||
containerID string,
|
||||
reader io.ReadCloser,
|
||||
stream string,
|
||||
) (time.Time, error) {
|
||||
defer reader.Close()
|
||||
|
||||
tags := make(map[string]string, len(baseTags)+1)
|
||||
for k, v := range baseTags {
|
||||
tags[k] = v
|
||||
}
|
||||
tags["stream"] = stream
|
||||
|
||||
r := bufio.NewReaderSize(reader, 64*1024)
|
||||
|
||||
var lastTS time.Time
|
||||
for {
|
||||
line, err := r.ReadBytes('\n')
|
||||
|
||||
if len(line) != 0 {
|
||||
ts, message, err := parseLine(line)
|
||||
if err != nil {
|
||||
acc.AddError(err)
|
||||
} else {
|
||||
acc.AddFields("docker_log", map[string]interface{}{
|
||||
"container_id": containerID,
|
||||
"message": message,
|
||||
}, tags, ts)
|
||||
}
|
||||
|
||||
// Store the last processed timestamp
|
||||
if ts.After(lastTS) {
|
||||
lastTS = ts
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return lastTS, nil
|
||||
}
|
||||
return time.Time{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tailMultiplexed(
|
||||
acc telegraf.Accumulator,
|
||||
tags map[string]string,
|
||||
containerID string,
|
||||
src io.ReadCloser,
|
||||
) (time.Time, error) {
|
||||
outReader, outWriter := io.Pipe()
|
||||
errReader, errWriter := io.Pipe()
|
||||
|
||||
var tsStdout, tsStderr time.Time
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var err error
|
||||
tsStdout, err = tailStream(acc, tags, containerID, outReader, "stdout")
|
||||
if err != nil {
|
||||
acc.AddError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var err error
|
||||
tsStderr, err = tailStream(acc, tags, containerID, errReader, "stderr")
|
||||
if err != nil {
|
||||
acc.AddError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err := stdcopy.StdCopy(outWriter, errWriter, src)
|
||||
|
||||
// Ignore the returned errors as we cannot do anything if the closing fails
|
||||
_ = outWriter.Close()
|
||||
_ = errWriter.Close()
|
||||
_ = src.Close()
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if tsStdout.After(tsStderr) {
|
||||
return tsStdout, nil
|
||||
}
|
||||
return tsStderr, nil
|
||||
}
|
||||
|
||||
// Following few functions have been inherited from telegraf docker input plugin
|
||||
func (d *DockerLogs) createContainerFilters() error {
|
||||
containerFilter, err := filter.NewIncludeExcludeFilter(d.ContainerInclude, d.ContainerExclude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.containerFilter = containerFilter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerLogs) createLabelFilters() error {
|
||||
labelFilter, err := filter.NewIncludeExcludeFilter(d.LabelInclude, d.LabelExclude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.labelFilter = labelFilter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerLogs) createContainerStateFilters() error {
|
||||
if len(d.ContainerStateInclude) == 0 && len(d.ContainerStateExclude) == 0 {
|
||||
d.ContainerStateInclude = []string{"running"}
|
||||
}
|
||||
stateFilter, err := filter.NewIncludeExcludeFilter(d.ContainerStateInclude, d.ContainerStateExclude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.stateFilter = stateFilter
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostnameFromID(id string) string {
|
||||
if len(id) > 12 {
|
||||
return id[0:12]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func init() {
|
||||
inputs.Add("docker_log", func() telegraf.Input {
|
||||
return &DockerLogs{
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
Endpoint: defaultEndpoint,
|
||||
newEnvClient: newEnvClient,
|
||||
newClient: newClient,
|
||||
containerList: make(map[string]context.CancelFunc),
|
||||
}
|
||||
})
|
||||
}
|
178
plugins/inputs/docker_log/docker_log_test.go
Normal file
178
plugins/inputs/docker_log/docker_log_test.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
package docker_log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
ContainerListF func() ([]container.Summary, error)
|
||||
ContainerInspectF func() (container.InspectResponse, error)
|
||||
ContainerLogsF func() (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (c *mockClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
|
||||
return c.ContainerListF()
|
||||
}
|
||||
|
||||
func (c *mockClient) ContainerInspect(context.Context, string) (container.InspectResponse, error) {
|
||||
return c.ContainerInspectF()
|
||||
}
|
||||
|
||||
func (c *mockClient) ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, error) {
|
||||
return c.ContainerLogsF()
|
||||
}
|
||||
|
||||
type response struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (*response) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustParse(layout, value string) time.Time {
|
||||
tm, err := time.Parse(layout, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tm
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
client *mockClient
|
||||
expected []telegraf.Metric
|
||||
}{
|
||||
{
|
||||
name: "no containers",
|
||||
client: &mockClient{
|
||||
ContainerListF: func() ([]container.Summary, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one container tty",
|
||||
client: &mockClient{
|
||||
ContainerListF: func() ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
{
|
||||
ID: "deadbeef",
|
||||
Names: []string{"/telegraf"},
|
||||
Image: "influxdata/telegraf:1.11.0",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ContainerInspectF: func() (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
Config: &container.Config{
|
||||
Tty: true,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ContainerLogsF: func() (io.ReadCloser, error) {
|
||||
return &response{Reader: bytes.NewBufferString("2020-04-28T18:43:16.432691200Z hello\n")}, nil
|
||||
},
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"docker_log",
|
||||
map[string]string{
|
||||
"container_name": "telegraf",
|
||||
"container_image": "influxdata/telegraf",
|
||||
"container_version": "1.11.0",
|
||||
"stream": "tty",
|
||||
"source": "deadbeef",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"container_id": "deadbeef",
|
||||
"message": "hello",
|
||||
},
|
||||
mustParse(time.RFC3339Nano, "2020-04-28T18:43:16.432691200Z"),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one container multiplex",
|
||||
client: &mockClient{
|
||||
ContainerListF: func() ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
{
|
||||
ID: "deadbeef",
|
||||
Names: []string{"/telegraf"},
|
||||
Image: "influxdata/telegraf:1.11.0",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ContainerInspectF: func() (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
Config: &container.Config{
|
||||
Tty: false,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
ContainerLogsF: func() (io.ReadCloser, error) {
|
||||
var buf bytes.Buffer
|
||||
w := stdcopy.NewStdWriter(&buf, stdcopy.Stdout)
|
||||
_, err := w.Write([]byte("2020-04-28T18:42:16.432691200Z hello from stdout"))
|
||||
return &response{Reader: &buf}, err
|
||||
},
|
||||
},
|
||||
expected: []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"docker_log",
|
||||
map[string]string{
|
||||
"container_name": "telegraf",
|
||||
"container_image": "influxdata/telegraf",
|
||||
"container_version": "1.11.0",
|
||||
"stream": "stdout",
|
||||
"source": "deadbeef",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"container_id": "deadbeef",
|
||||
"message": "hello from stdout",
|
||||
},
|
||||
mustParse(time.RFC3339Nano, "2020-04-28T18:42:16.432691200Z"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var acc testutil.Accumulator
|
||||
plugin := &DockerLogs{
|
||||
Timeout: config.Duration(time.Second * 5),
|
||||
newClient: func(string, *tls.Config) (dockerClient, error) { return tt.client, nil },
|
||||
containerList: make(map[string]context.CancelFunc),
|
||||
IncludeSourceTag: true,
|
||||
}
|
||||
|
||||
err := plugin.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = plugin.Gather(&acc)
|
||||
require.NoError(t, err)
|
||||
|
||||
acc.Wait(len(tt.expected))
|
||||
plugin.Stop()
|
||||
|
||||
require.Nil(t, acc.Errors) // no errors during gathering
|
||||
|
||||
testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics())
|
||||
})
|
||||
}
|
||||
}
|
39
plugins/inputs/docker_log/sample.conf
Normal file
39
plugins/inputs/docker_log/sample.conf
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Read logging output from the Docker engine
|
||||
[[inputs.docker_log]]
|
||||
## Docker Endpoint
|
||||
## To use TCP, set endpoint = "tcp://[ip]:[port]"
|
||||
## To use environment variables (ie, docker-machine), set endpoint = "ENV"
|
||||
# endpoint = "unix:///var/run/docker.sock"
|
||||
|
||||
## When true, container logs are read from the beginning; otherwise reading
|
||||
## 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
|
||||
|
||||
## Timeout for Docker API calls.
|
||||
# timeout = "5s"
|
||||
|
||||
## Containers to include and exclude. Globs accepted.
|
||||
## Note that an empty array for both will include all containers
|
||||
# container_name_include = []
|
||||
# container_name_exclude = []
|
||||
|
||||
## Container states to include and exclude. Globs accepted.
|
||||
## When empty only containers in the "running" state will be captured.
|
||||
# container_state_include = []
|
||||
# container_state_exclude = []
|
||||
|
||||
## docker labels to include and exclude as tags. Globs accepted.
|
||||
## Note that an empty array for both will include all labels as tags
|
||||
# docker_label_include = []
|
||||
# docker_label_exclude = []
|
||||
|
||||
## Set the source tag for the metrics to the container ID hostname, eg first 12 chars
|
||||
source_tag = false
|
||||
|
||||
## Optional TLS Config
|
||||
# tls_ca = "/etc/telegraf/ca.pem"
|
||||
# tls_cert = "/etc/telegraf/cert.pem"
|
||||
# tls_key = "/etc/telegraf/key.pem"
|
||||
## Use TLS but skip chain & host verification
|
||||
# insecure_skip_verify = false
|
Loading…
Add table
Add a link
Reference in a new issue