From 4716eea0db244948ea3e4596a240d681cd2602dc Mon Sep 17 00:00:00 2001 From: Karsten Jeschkies Date: Tue, 25 Jan 2022 11:05:29 +0100 Subject: [PATCH] Provide Docker target and discovery in Promtail. (#4911) **What this PR does / why we need it**: This patch adds support to fetch Docker container logs through the Docker daemon API. This should be more robust than the Loki Docker driver or scraping the logs files. The new Docker target will also collect meta information of the scraped containers. **Which issue(s) this PR fixes**: Addresses #2361 Closes #4703 **Special notes for your reviewer**: **Checklist** - [x] Documentation added - [x] Tests updated - [x] Add an entry in the `CHANGELOG.md` about the changes. --- CHANGELOG.md | 3 +- .../pkg/promtail/scrapeconfig/scrapeconfig.go | 26 +- .../pkg/promtail/targets/docker/metrics.go | 38 +++ clients/pkg/promtail/targets/docker/target.go | 231 ++++++++++++++++++ .../promtail/targets/docker/target_group.go | 144 +++++++++++ .../promtail/targets/docker/target_test.go | 78 ++++++ .../promtail/targets/docker/targetmanager.go | 141 +++++++++++ .../targets/docker/targetmanager_test.go | 112 +++++++++ .../promtail/targets/docker/testdata/flog.log | Bin 0 -> 164563 bytes clients/pkg/promtail/targets/manager.go | 29 +++ clients/pkg/promtail/targets/target/target.go | 3 + docs/sources/clients/docker-driver/_index.md | 31 +-- .../sources/clients/promtail/configuration.md | 115 +++++++++ .../docker/docker/pkg/stdcopy/stdcopy.go | 190 ++++++++++++++ vendor/modules.txt | 1 + 15 files changed, 1099 insertions(+), 43 deletions(-) create mode 100644 clients/pkg/promtail/targets/docker/metrics.go create mode 100644 clients/pkg/promtail/targets/docker/target.go create mode 100644 clients/pkg/promtail/targets/docker/target_group.go create mode 100644 clients/pkg/promtail/targets/docker/target_test.go create mode 100644 clients/pkg/promtail/targets/docker/targetmanager.go create mode 100644 clients/pkg/promtail/targets/docker/targetmanager_test.go create mode 100644 clients/pkg/promtail/targets/docker/testdata/flog.log create mode 100644 vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0b3c66b4..105851a23f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ ## Main +* [4911](https://github.com/grafana/loki/pull/4911) **jeschkies**: Support Docker service discovery in Promtail. +* [5107](https://github.com/grafana/loki/pull/5107) **chaudum** Fix bug in fluentd plugin that caused log lines containing non UTF-8 characters to be dropped. * [5187](https://github.com/grafana/loki/pull/5187) **aknuds1** Rename metric `cortex_experimental_features_in_use_total` to `loki_experimental_features_in_use_total` and metric `log_messages_total` to `loki_log_messages_total`. * [5170](https://github.com/grafana/loki/pull/5170) **chaudum** Fix deadlock in Promtail caused when targets got removed from a target group by the discovery manager. * [5163](https://github.com/grafana/loki/pull/5163) **chaudum** Fix regression in fluentd plugin introduced with #5107 that caused `NoMethodError` when parsing non-string values of log lines. * [5144](https://github.com/grafana/loki/pull/5144) **dannykopping** Ruler: fix remote write basic auth credentials. -* [5107](https://github.com/grafana/loki/pull/5107) **chaudum** Fix bug in fluentd plugin that caused log lines containing non UTF-8 characters to be dropped. * [5091](https://github.com/grafana/loki/pull/5091) **owen-d**: Changes `ingester.concurrent-flushes` default to 32 * [4879](https://github.com/grafana/loki/pull/4879) **cyriltovena**: LogQL: add __line__ function to | line_format template. * [5081](https://github.com/grafana/loki/pull/5081) **SasSwart**: Add the option to configure memory ballast for Loki diff --git a/clients/pkg/promtail/scrapeconfig/scrapeconfig.go b/clients/pkg/promtail/scrapeconfig/scrapeconfig.go index 44b24d436b..738723ec5c 100644 --- a/clients/pkg/promtail/scrapeconfig/scrapeconfig.go +++ b/clients/pkg/promtail/scrapeconfig/scrapeconfig.go @@ -33,18 +33,20 @@ import ( // Config describes a job to scrape. type Config struct { - JobName string `yaml:"job_name,omitempty"` - PipelineStages stages.PipelineStages `yaml:"pipeline_stages,omitempty"` - JournalConfig *JournalTargetConfig `yaml:"journal,omitempty"` - SyslogConfig *SyslogTargetConfig `yaml:"syslog,omitempty"` - GcplogConfig *GcplogTargetConfig `yaml:"gcplog,omitempty"` - PushConfig *PushTargetConfig `yaml:"loki_push_api,omitempty"` - WindowsConfig *WindowsEventsTargetConfig `yaml:"windows_events,omitempty"` - KafkaConfig *KafkaTargetConfig `yaml:"kafka,omitempty"` - GelfConfig *GelfTargetConfig `yaml:"gelf,omitempty"` - CloudflareConfig *CloudflareConfig `yaml:"cloudflare,omitempty"` - RelabelConfigs []*relabel.Config `yaml:"relabel_configs,omitempty"` - ServiceDiscoveryConfig ServiceDiscoveryConfig `yaml:",inline"` + JobName string `yaml:"job_name,omitempty"` + PipelineStages stages.PipelineStages `yaml:"pipeline_stages,omitempty"` + JournalConfig *JournalTargetConfig `yaml:"journal,omitempty"` + SyslogConfig *SyslogTargetConfig `yaml:"syslog,omitempty"` + GcplogConfig *GcplogTargetConfig `yaml:"gcplog,omitempty"` + PushConfig *PushTargetConfig `yaml:"loki_push_api,omitempty"` + WindowsConfig *WindowsEventsTargetConfig `yaml:"windows_events,omitempty"` + KafkaConfig *KafkaTargetConfig `yaml:"kafka,omitempty"` + GelfConfig *GelfTargetConfig `yaml:"gelf,omitempty"` + CloudflareConfig *CloudflareConfig `yaml:"cloudflare,omitempty"` + RelabelConfigs []*relabel.Config `yaml:"relabel_configs,omitempty"` + // List of Docker service discovery configurations. + DockerSDConfigs []*moby.DockerSDConfig `yaml:"docker_sd_configs,omitempty"` + ServiceDiscoveryConfig ServiceDiscoveryConfig `yaml:",inline"` } type ServiceDiscoveryConfig struct { diff --git a/clients/pkg/promtail/targets/docker/metrics.go b/clients/pkg/promtail/targets/docker/metrics.go new file mode 100644 index 0000000000..9d57493e5e --- /dev/null +++ b/clients/pkg/promtail/targets/docker/metrics.go @@ -0,0 +1,38 @@ +package docker + +import "github.com/prometheus/client_golang/prometheus" + +// Metrics holds a set of Docker target metrics. +type Metrics struct { + reg prometheus.Registerer + + dockerEntries prometheus.Counter + dockerErrors prometheus.Counter +} + +// NewMetrics creates a new set of Docker target metrics. If reg is non-nil, the +// metrics will be registered. +func NewMetrics(reg prometheus.Registerer) *Metrics { + var m Metrics + m.reg = reg + + m.dockerEntries = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "promtail", + Name: "docker_target_entries_total", + Help: "Total number of successful entries sent to the Docker target", + }) + m.dockerErrors = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "promtail", + Name: "docker_target_parsing_errors_total", + Help: "Total number of parsing errors while receiving Docker messages", + }) + + if reg != nil { + reg.MustRegister( + m.dockerEntries, + m.dockerErrors, + ) + } + + return &m +} diff --git a/clients/pkg/promtail/targets/docker/target.go b/clients/pkg/promtail/targets/docker/target.go new file mode 100644 index 0000000000..a9f41a92b0 --- /dev/null +++ b/clients/pkg/promtail/targets/docker/target.go @@ -0,0 +1,231 @@ +package docker + +import ( + "bufio" + "context" + "fmt" + "io" + "strconv" + "strings" + "sync" + "time" + + docker_types "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/relabel" + "go.uber.org/atomic" + + "github.com/grafana/loki/clients/pkg/promtail/api" + "github.com/grafana/loki/clients/pkg/promtail/positions" + "github.com/grafana/loki/clients/pkg/promtail/targets/target" + + "github.com/grafana/loki/pkg/logproto" +) + +type Target struct { + logger log.Logger + handler api.EntryHandler + since int64 + positions positions.Positions + containerName string + labels model.LabelSet + relabelConfig []*relabel.Config + metrics *Metrics + + cancel context.CancelFunc + client client.APIClient + wg sync.WaitGroup + running *atomic.Bool + err error +} + +func NewTarget( + metrics *Metrics, + logger log.Logger, + handler api.EntryHandler, + position positions.Positions, + containerName string, + labels model.LabelSet, + relabelConfig []*relabel.Config, + client client.APIClient, +) (*Target, error) { + + pos, err := position.Get(positions.CursorKey(containerName)) + if err != nil { + return nil, err + } + var since int64 + if pos != 0 { + since = pos + } + + ctx, cancel := context.WithCancel(context.Background()) + t := &Target{ + logger: logger, + handler: handler, + since: since, + positions: position, + containerName: containerName, + labels: labels, + relabelConfig: relabelConfig, + metrics: metrics, + + cancel: cancel, + client: client, + running: atomic.NewBool(false), + } + go t.processLoop(ctx) + return t, nil +} + +func (t *Target) processLoop(ctx context.Context) { + t.wg.Add(1) + defer t.wg.Done() + t.running.Store(true) + + opts := docker_types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + Timestamps: true, + Since: strconv.FormatInt(t.since, 10), + } + + logs, err := t.client.ContainerLogs(ctx, t.containerName, opts) + if err != nil { + level.Error(t.logger).Log("msg", "could not fetch logs for container", "container", t.containerName, "err", err) + t.err = err + return + } + + // Start transferring + rstdout, wstdout := io.Pipe() + rstderr, wstderr := io.Pipe() + t.wg.Add(1) + go func() { + defer func() { + t.wg.Done() + wstdout.Close() + wstderr.Close() + t.Stop() + }() + + written, err := stdcopy.StdCopy(wstdout, wstderr, logs) + if err != nil { + level.Warn(t.logger).Log("msg", "could not transfer logs", "written", written, "container", t.containerName, "err", err) + } else { + level.Info(t.logger).Log("msg", "finished transferring logs", "written", written, "container", t.containerName) + } + }() + + // Start processing + t.wg.Add(2) + go t.process(rstdout, "stdout") + go t.process(rstderr, "stderr") + + // Wait until done + <-ctx.Done() + t.running.Store(false) + logs.Close() + level.Debug(t.logger).Log("msg", "done processing Docker logs", "container", t.containerName) +} + +// extractTs tries for read the timestamp from the beginning of the log line. +// It's expected to follow the format 2006-01-02T15:04:05.999999999Z07:00. +func extractTs(line string) (time.Time, string, error) { + pair := strings.SplitN(line, " ", 2) + if len(pair) != 2 { + return time.Now(), line, fmt.Errorf("Could not find timestamp in '%s'", line) + } + ts, err := time.Parse("2006-01-02T15:04:05.999999999Z07:00", pair[0]) + if err != nil { + return time.Now(), line, fmt.Errorf("Could not parse timestamp from '%s': %w", pair[0], err) + } + return ts, pair[1], nil +} + +func (t *Target) process(r io.Reader, logStream string) { + defer func() { + t.wg.Done() + }() + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + ts, line, err := extractTs(line) + if err != nil { + level.Error(t.logger).Log("msg", "could not extract timestamp, skipping line", "err", err) + t.metrics.dockerErrors.Inc() + continue + } + + // Add all labels from the config, relabel and filter them. + lb := labels.NewBuilder(nil) + for k, v := range t.labels { + lb.Set(string(k), string(v)) + } + lb.Set(dockerLabelLogStream, logStream) + processed := relabel.Process(lb.Labels(), t.relabelConfig...) + + filtered := make(model.LabelSet) + for _, lbl := range processed { + if strings.HasPrefix(lbl.Name, "__") { + continue + } + filtered[model.LabelName(lbl.Name)] = model.LabelValue(lbl.Value) + } + + t.handler.Chan() <- api.Entry{ + Labels: filtered, + Entry: logproto.Entry{ + Timestamp: ts, + Line: line, + }, + } + t.metrics.dockerEntries.Inc() + t.positions.Put(positions.CursorKey(t.containerName), ts.Unix()) + } + + err := scanner.Err() + if err != nil { + level.Warn(t.logger).Log("msg", "finished scanning logs lines with an error", "err", err) + } + +} + +func (t *Target) Stop() { + t.cancel() + t.wg.Wait() + level.Debug(t.logger).Log("msg", "stopped Docker target", "container", t.containerName) +} + +func (t *Target) Type() target.TargetType { + return target.DockerTargetType +} + +func (t *Target) Ready() bool { + return t.running.Load() +} + +func (t *Target) DiscoveredLabels() model.LabelSet { + return t.labels +} + +func (t *Target) Labels() model.LabelSet { + return t.labels +} + +// Details returns target-specific details. +func (t *Target) Details() interface{} { + return map[string]string{ + "id": t.containerName, + "error": t.err.Error(), + "position": t.positions.GetString(positions.CursorKey(t.containerName)), + "running": strconv.FormatBool(t.running.Load()), + } +} diff --git a/clients/pkg/promtail/targets/docker/target_group.go b/clients/pkg/promtail/targets/docker/target_group.go new file mode 100644 index 0000000000..c9f45e95d4 --- /dev/null +++ b/clients/pkg/promtail/targets/docker/target_group.go @@ -0,0 +1,144 @@ +package docker + +import ( + "fmt" + "sync" + + "github.com/docker/docker/client" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/grafana/loki/clients/pkg/promtail/api" + "github.com/grafana/loki/clients/pkg/promtail/positions" + "github.com/grafana/loki/clients/pkg/promtail/targets/target" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/discovery/targetgroup" + "github.com/prometheus/prometheus/model/relabel" +) + +const DockerSource = "Docker" + +// targetGroup manages all container targets of one Docker daemon. +type targetGroup struct { + metrics *Metrics + logger log.Logger + positions positions.Positions + entryHandler api.EntryHandler + defaultLabels model.LabelSet + relabelConfig []*relabel.Config + host string + client client.APIClient + + mtx sync.Mutex + targets map[string]*Target +} + +func (tg *targetGroup) sync(groups []*targetgroup.Group) { + tg.mtx.Lock() + defer tg.mtx.Unlock() + + for _, group := range groups { + if group.Source != DockerSource { + continue + } + + for _, t := range group.Targets { + containerID, ok := t[dockerLabelContainerID] + if !ok { + level.Debug(tg.logger).Log("msg", "Docker target did not include container ID") + continue + } + + err := tg.addTarget(string(containerID), t) + if err != nil { + level.Error(tg.logger).Log("msg", "could not add target", "containerID", containerID, "err", err) + } + } + } +} + +// addTarget checks whether the container with given id is already known. If not it's added to the this group +func (tg *targetGroup) addTarget(id string, discoveredLabels model.LabelSet) error { + if tg.client == nil { + var err error + opts := []client.Opt{ + client.WithHost(tg.host), + client.WithAPIVersionNegotiation(), + } + tg.client, err = client.NewClientWithOpts(opts...) + if err != nil { + level.Error(tg.logger).Log("msg", "could not create new Docker client", "err", err) + return err + } + } + + _, ok := tg.targets[id] + if ok { + level.Debug(tg.logger).Log("msg", "ignoring container that is already being scraped", "container", id) + return nil + } + + t, err := NewTarget( + tg.metrics, + log.With(tg.logger, "target", fmt.Sprintf("docker/%s", id)), + tg.entryHandler, + tg.positions, + id, + discoveredLabels.Merge(tg.defaultLabels), + tg.relabelConfig, + tg.client, + ) + if err != nil { + return err + } + tg.targets[id] = t + level.Error(tg.logger).Log("msg", "added Docker target", "containerID", id) + return nil +} + +// Ready returns true if at least one target is running. +func (tg *targetGroup) Ready() bool { + tg.mtx.Lock() + defer tg.mtx.Unlock() + + for _, t := range tg.targets { + if t.Ready() { + return true + } + } + + return true +} + +// Stop all targets +func (tg *targetGroup) Stop() { + tg.mtx.Lock() + defer tg.mtx.Unlock() + + for _, t := range tg.targets { + t.Stop() + } + tg.entryHandler.Stop() +} + +// ActiveTargets return all targets that are ready. +func (tg *targetGroup) ActiveTargets() []target.Target { + tg.mtx.Lock() + defer tg.mtx.Unlock() + + result := make([]target.Target, 0, len(tg.targets)) + for _, t := range tg.targets { + if t.Ready() { + result = append(result, t) + } + } + return result +} + +// AllTargets returns all targets of this group. +func (tg *targetGroup) AllTargets() []target.Target { + result := make([]target.Target, 0, len(tg.targets)) + for _, t := range tg.targets { + result = append(result, t) + } + return result +} diff --git a/clients/pkg/promtail/targets/docker/target_test.go b/clients/pkg/promtail/targets/docker/target_test.go new file mode 100644 index 0000000000..91e45832ac --- /dev/null +++ b/clients/pkg/promtail/targets/docker/target_test.go @@ -0,0 +1,78 @@ +package docker + +import ( + "net/http" + "net/http/httptest" + "os" + "sort" + "testing" + "time" + + "github.com/docker/docker/client" + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/relabel" + "github.com/stretchr/testify/require" + + "github.com/grafana/loki/clients/pkg/promtail/client/fake" + "github.com/grafana/loki/clients/pkg/promtail/positions" +) + +func Test_DockerTarget(t *testing.T) { + h := func(w http.ResponseWriter, r *http.Request) { + dat, err := os.ReadFile("testdata/flog.log") + require.NoError(t, err) + _, err = w.Write(dat) + require.NoError(t, err) + } + + ts := httptest.NewServer(http.HandlerFunc(h)) + defer ts.Close() + + w := log.NewSyncWriter(os.Stderr) + logger := log.NewLogfmtLogger(w) + entryHandler := fake.New(func() {}) + client, err := client.NewClientWithOpts(client.WithHost(ts.URL)) + require.NoError(t, err) + + ps, err := positions.New(logger, positions.Config{ + SyncPeriod: 10 * time.Second, + PositionsFile: t.TempDir() + "/positions.yml", + }) + require.NoError(t, err) + + _, err = NewTarget( + NewMetrics(prometheus.NewRegistry()), + logger, + entryHandler, + ps, + "flog", + model.LabelSet{"job": "docker"}, + []*relabel.Config{}, + client, + ) + require.NoError(t, err) + + require.Eventually(t, func() bool { + return len(entryHandler.Received()) >= 5 + }, 5*time.Second, 100*time.Millisecond) + + received := entryHandler.Received() + sort.Slice(received, func(i, j int) bool { + return received[i].Timestamp.Before(received[j].Timestamp) + }) + + expectedLines := []string{ + "5.3.69.55 - - [09/Dec/2021:09:15:02 +0000] \"HEAD /brand/users/clicks-and-mortar/front-end HTTP/2.0\" 503 27087", + "101.54.183.185 - - [09/Dec/2021:09:15:03 +0000] \"POST /next-generation HTTP/1.0\" 416 11468", + "69.27.137.160 - runolfsdottir2670 [09/Dec/2021:09:15:03 +0000] \"HEAD /content/visionary/engineer/cultivate HTTP/1.1\" 302 2975", + "28.104.242.74 - - [09/Dec/2021:09:15:03 +0000] \"PATCH /value-added/cultivate/systems HTTP/2.0\" 405 11843", + "150.187.51.54 - satterfield1852 [09/Dec/2021:09:15:03 +0000] \"GET /incentivize/deliver/innovative/cross-platform HTTP/1.1\" 301 13032", + } + actualLines := make([]string, 0, 5) + for _, entry := range received[:5] { + actualLines = append(actualLines, entry.Line) + } + require.ElementsMatch(t, actualLines, expectedLines) +} diff --git a/clients/pkg/promtail/targets/docker/targetmanager.go b/clients/pkg/promtail/targets/docker/targetmanager.go new file mode 100644 index 0000000000..3fb4c32a03 --- /dev/null +++ b/clients/pkg/promtail/targets/docker/targetmanager.go @@ -0,0 +1,141 @@ +package docker + +import ( + "context" + "fmt" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/grafana/loki/clients/pkg/logentry/stages" + "github.com/grafana/loki/clients/pkg/promtail/api" + "github.com/grafana/loki/clients/pkg/promtail/positions" + "github.com/grafana/loki/clients/pkg/promtail/scrapeconfig" + "github.com/grafana/loki/clients/pkg/promtail/targets/target" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/discovery" + + "github.com/grafana/loki/pkg/util" +) + +const ( + // See github.com/prometheus/prometheus/discovery/moby + dockerLabel = model.MetaLabelPrefix + "docker_" + dockerLabelContainerPrefix = dockerLabel + "container_" + dockerLabelContainerID = dockerLabelContainerPrefix + "id" + dockerLabelLogStream = dockerLabelContainerPrefix + "_log_stream" +) + +type TargetManager struct { + metrics *Metrics + logger log.Logger + positions positions.Positions + cancel context.CancelFunc + manager *discovery.Manager + pushClient api.EntryHandler + groups map[string]*targetGroup +} + +func NewTargetManager( + metrics *Metrics, + logger log.Logger, + positions positions.Positions, + pushClient api.EntryHandler, + scrapeConfigs []scrapeconfig.Config, +) (*TargetManager, error) { + ctx, cancel := context.WithCancel(context.Background()) + tm := &TargetManager{ + metrics: metrics, + logger: logger, + cancel: cancel, + positions: positions, + manager: discovery.NewManager(ctx, log.With(logger, "component", "docker_discovery")), + pushClient: pushClient, + groups: make(map[string]*targetGroup), + } + configs := map[string]discovery.Configs{} + for _, cfg := range scrapeConfigs { + if cfg.DockerSDConfigs != nil { + pipeline, err := stages.NewPipeline( + log.With(logger, "component", "docker_pipeline"), + cfg.PipelineStages, + &cfg.JobName, + metrics.reg, + ) + if err != nil { + return nil, err + } + + for _, sdConfig := range cfg.DockerSDConfigs { + syncerKey := fmt.Sprintf("%s/%s:%d", cfg.JobName, sdConfig.Host, sdConfig.Port) + _, ok := tm.groups[syncerKey] + if !ok { + tm.groups[syncerKey] = &targetGroup{ + metrics: metrics, + logger: logger, + positions: positions, + targets: make(map[string]*Target), + entryHandler: pipeline.Wrap(pushClient), + defaultLabels: model.LabelSet{}, + relabelConfig: cfg.RelabelConfigs, + host: sdConfig.Host, + } + } + configs[syncerKey] = append(configs[syncerKey], sdConfig) + } + } else { + level.Debug(tm.logger).Log("msg", "Docker service discovery configs are empty") + } + } + + go tm.run() + go util.LogError("running target manager", tm.manager.Run) + + return tm, tm.manager.ApplyConfig(configs) +} + +// run listens on the service discovery and adds new targets. +func (tm *TargetManager) run() { + for targetGroups := range tm.manager.SyncCh() { + for jobName, groups := range targetGroups { + tg, ok := tm.groups[jobName] + if !ok { + level.Debug(tm.logger).Log("msg", "unknown target for job", "job", jobName) + continue + } + tg.sync(groups) + } + } +} + +// Ready returns true if at least one Docker target is active. +func (tm *TargetManager) Ready() bool { + for _, s := range tm.groups { + if s.Ready() { + return true + } + } + return false +} + +func (tm *TargetManager) Stop() { + tm.cancel() + for _, s := range tm.groups { + s.Stop() + } +} + +func (tm *TargetManager) ActiveTargets() map[string][]target.Target { + result := make(map[string][]target.Target, len(tm.groups)) + for k, s := range tm.groups { + result[k] = s.ActiveTargets() + } + return result +} + +func (tm *TargetManager) AllTargets() map[string][]target.Target { + result := make(map[string][]target.Target, len(tm.groups)) + for k, s := range tm.groups { + result[k] = s.AllTargets() + } + return result +} diff --git a/clients/pkg/promtail/targets/docker/targetmanager_test.go b/clients/pkg/promtail/targets/docker/targetmanager_test.go new file mode 100644 index 0000000000..f60bf5e3ea --- /dev/null +++ b/clients/pkg/promtail/targets/docker/targetmanager_test.go @@ -0,0 +1,112 @@ +package docker + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "sort" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/discovery/moby" + "github.com/stretchr/testify/require" + + "github.com/grafana/loki/clients/pkg/promtail/client/fake" + "github.com/grafana/loki/clients/pkg/promtail/positions" + "github.com/grafana/loki/clients/pkg/promtail/scrapeconfig" +) + +func Test_TargetManager(t *testing.T) { + h := func(w http.ResponseWriter, r *http.Request) { + switch path := r.URL.Path; { + case path == "/_ping": + _, err := w.Write([]byte("OK")) + require.NoError(t, err) + case strings.HasSuffix(path, "/containers/json"): + // Serve container list + w.Header().Set("Content-Type", "application/json") + containerResponse := []types.Container{{ + ID: "1234", + Names: []string{"flog"}, + NetworkSettings: &types.SummaryNetworkSettings{ + Networks: map[string]*network.EndpointSettings{ + "foo": { + NetworkID: "my_network", + IPAddress: "127.0.0.1", + }, + }, + }, + }} + err := json.NewEncoder(w).Encode(containerResponse) + require.NoError(t, err) + case strings.HasSuffix(path, "/networks"): + // Serve networks + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode([]types.NetworkResource{}) + require.NoError(t, err) + default: + // Serve container logs + dat, err := os.ReadFile("testdata/flog.log") + require.NoError(t, err) + _, err = w.Write(dat) + require.NoError(t, err) + } + } + dockerDaemonMock := httptest.NewServer(http.HandlerFunc(h)) + defer dockerDaemonMock.Close() + + w := log.NewSyncWriter(os.Stderr) + logger := log.NewLogfmtLogger(w) + entryHandler := fake.New(func() {}) + cfgs := []scrapeconfig.Config{{ + DockerSDConfigs: []*moby.DockerSDConfig{{ + Host: dockerDaemonMock.URL, + RefreshInterval: model.Duration(100 * time.Millisecond), + }}, + }} + + ps, err := positions.New(logger, positions.Config{ + SyncPeriod: 10 * time.Second, + PositionsFile: t.TempDir() + "/positions.yml", + }) + require.NoError(t, err) + + ta, err := NewTargetManager( + NewMetrics(prometheus.NewRegistry()), + logger, + ps, + entryHandler, + cfgs, + ) + require.NoError(t, err) + require.True(t, ta.Ready()) + + require.Eventually(t, func() bool { + return len(entryHandler.Received()) >= 5 + }, 20*time.Second, 100*time.Millisecond) + + received := entryHandler.Received() + sort.Slice(received, func(i, j int) bool { + return received[i].Timestamp.Before(received[j].Timestamp) + }) + + expectedLines := []string{ + "5.3.69.55 - - [09/Dec/2021:09:15:02 +0000] \"HEAD /brand/users/clicks-and-mortar/front-end HTTP/2.0\" 503 27087", + "101.54.183.185 - - [09/Dec/2021:09:15:03 +0000] \"POST /next-generation HTTP/1.0\" 416 11468", + "69.27.137.160 - runolfsdottir2670 [09/Dec/2021:09:15:03 +0000] \"HEAD /content/visionary/engineer/cultivate HTTP/1.1\" 302 2975", + "28.104.242.74 - - [09/Dec/2021:09:15:03 +0000] \"PATCH /value-added/cultivate/systems HTTP/2.0\" 405 11843", + "150.187.51.54 - satterfield1852 [09/Dec/2021:09:15:03 +0000] \"GET /incentivize/deliver/innovative/cross-platform HTTP/1.1\" 301 13032", + } + actualLines := make([]string, 0, 5) + for _, entry := range received[:5] { + actualLines = append(actualLines, entry.Line) + } + require.ElementsMatch(t, actualLines, expectedLines) +} diff --git a/clients/pkg/promtail/targets/docker/testdata/flog.log b/clients/pkg/promtail/targets/docker/testdata/flog.log new file mode 100644 index 0000000000000000000000000000000000000000..a3370e02d89a1018484972f51146655fe71b311a GIT binary patch literal 164563 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 literal 0 HcmV?d00001 diff --git a/clients/pkg/promtail/targets/manager.go b/clients/pkg/promtail/targets/manager.go index 97842eee5c..19c703d1b0 100644 --- a/clients/pkg/promtail/targets/manager.go +++ b/clients/pkg/promtail/targets/manager.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/loki/clients/pkg/promtail/positions" "github.com/grafana/loki/clients/pkg/promtail/scrapeconfig" "github.com/grafana/loki/clients/pkg/promtail/targets/cloudflare" + "github.com/grafana/loki/clients/pkg/promtail/targets/docker" "github.com/grafana/loki/clients/pkg/promtail/targets/file" "github.com/grafana/loki/clients/pkg/promtail/targets/gcplog" "github.com/grafana/loki/clients/pkg/promtail/targets/gelf" @@ -34,6 +35,8 @@ const ( KafkaConfigs = "kafkaConfigs" GelfConfigs = "gelfConfigs" CloudflareConfigs = "cloudflareConfigs" + DockerConfigs = "dockerConfigs" + DockerSDConfigs = "dockerSDConfigs" ) type targetManager interface { @@ -91,6 +94,8 @@ func NewTargetManagers( targetScrapeConfigs[GelfConfigs] = append(targetScrapeConfigs[GelfConfigs], cfg) case cfg.CloudflareConfig != nil: targetScrapeConfigs[CloudflareConfigs] = append(targetScrapeConfigs[CloudflareConfigs], cfg) + case cfg.DockerSDConfigs != nil: + targetScrapeConfigs[DockerSDConfigs] = append(targetScrapeConfigs[DockerSDConfigs], cfg) default: return nil, fmt.Errorf("no valid target scrape config defined for %q", cfg.JobName) } @@ -116,6 +121,7 @@ func NewTargetManagers( gcplogMetrics *gcplog.Metrics gelfMetrics *gelf.Metrics cloudflareMetrics *cloudflare.Metrics + dockerMetrics *docker.Metrics ) if len(targetScrapeConfigs[FileScrapeConfigs]) > 0 { fileMetrics = file.NewMetrics(reg) @@ -132,6 +138,9 @@ func NewTargetManagers( if len(targetScrapeConfigs[CloudflareConfigs]) > 0 { cloudflareMetrics = cloudflare.NewMetrics(reg) } + if len(targetScrapeConfigs[DockerConfigs]) > 0 || len(targetScrapeConfigs[DockerSDConfigs]) > 0 { + dockerMetrics = docker.NewMetrics(reg) + } for target, scrapeConfigs := range targetScrapeConfigs { switch target { @@ -229,6 +238,26 @@ func NewTargetManagers( return nil, errors.Wrap(err, "failed to make cloudflare target manager") } targetManagers = append(targetManagers, cfTargetManager) + case DockerConfigs: + pos, err := getPositionFile() + if err != nil { + return nil, err + } + cfTargetManager, err := docker.NewTargetManager(dockerMetrics, logger, pos, client, scrapeConfigs) + if err != nil { + return nil, errors.Wrap(err, "failed to make Docker target manager") + } + targetManagers = append(targetManagers, cfTargetManager) + case DockerSDConfigs: + pos, err := getPositionFile() + if err != nil { + return nil, err + } + cfTargetManager, err := docker.NewTargetManager(dockerMetrics, logger, pos, client, scrapeConfigs) + if err != nil { + return nil, errors.Wrap(err, "failed to make Docker service discovery target manager") + } + targetManagers = append(targetManagers, cfTargetManager) default: return nil, errors.New("unknown scrape config") } diff --git a/clients/pkg/promtail/targets/target/target.go b/clients/pkg/promtail/targets/target/target.go index 8acf6aad61..ecb021c69f 100644 --- a/clients/pkg/promtail/targets/target/target.go +++ b/clients/pkg/promtail/targets/target/target.go @@ -38,6 +38,9 @@ const ( // CloudflareTargetType is a Cloudflare target CloudflareTargetType = TargetType("Cloudflare") + + // DockerTargetType is a Docker target + DockerTargetType = TargetType("Docker") ) // Target is a promtail scrape target diff --git a/docs/sources/clients/docker-driver/_index.md b/docs/sources/clients/docker-driver/_index.md index 39f4918893..c979b3a682 100644 --- a/docs/sources/clients/docker-driver/_index.md +++ b/docs/sources/clients/docker-driver/_index.md @@ -63,33 +63,4 @@ docker plugin rm loki The driver keeps all logs in memory and will drop log entries if Loki is not reachable and if the quantity of `max_retries` has been exceeded. To avoid the dropping of log entries, setting `max_retries` to zero allows unlimited retries; the drive will continue trying forever until Loki is again reachable. Trying forever may have undesired consequences, because the Docker daemon will wait for the Loki driver to process all logs of a container, until the container is removed. Thus, the Docker daemon might wait forever if the container is stuck. -This issue is avoided by using [Promtail](../promtail) with this configuration: - -```yaml -server: - disable: true - -positions: - filename: loki-positions.yml - -clients: - - url: http://ip_or_hostname_where_Loki_run:3100/loki/api/v1/push - # If using basic auth, configures the username and password sent. - basic_auth: - # The username to use for basic auth - username: - # The password to use for basic auth - password: - -scrape_configs: - - job_name: system - pipeline_stages: - - docker: {} - static_configs: - - labels: - job: docker - __path__: /var/lib/docker/containers/*/*-json.log - -``` - -This will enable Promtail to tail *all* Docker container logs and publish them to Loki. +Use Promtail's [Docker target](../promtail/configuration/#docker) or [Docker service discovery](../promtail/configuration/#docker_sd_config) to avoid this issue. diff --git a/docs/sources/clients/promtail/configuration.md b/docs/sources/clients/promtail/configuration.md index 55ec0a0932..23a438e8ca 100644 --- a/docs/sources/clients/promtail/configuration.md +++ b/docs/sources/clients/promtail/configuration.md @@ -358,6 +358,11 @@ consul_sd_configs: # running on the same host as Promtail. consulagent_sd_configs: [ - ... ] + +# Describes how to use the Docker daemon API to discover containers running on +# the same host as Promtail. +docker_sd_configs: + [ - ... ] ``` ### pipeline_stages @@ -1622,6 +1627,116 @@ users with thousands of services it can be more efficient to use the Consul API directly which has basic support for filtering nodes (currently by node metadata and a single tag). +### docker_sd_config + +Docker service discovery allows retrieving targets from a Docker daemon. +It will only watch containers of the Docker daemon referenced with the host parameter. Docker +service discovery should run on each node in a distributed setup. The containers must run with +either the [json-file](https://docs.docker.com/config/containers/logging/json-file/) +or [journald](https://docs.docker.com/config/containers/logging/journald/) logging driver. + +Please note that the discovery will not pick up finished containers. That means +Promtail will not scrape the remaining logs from finished containers after a restart. + +The configuration is inherited from [Prometheus' Docker service discovery](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#docker_sd_config). + +```yaml +# Address of the Docker daemon. Use unix:///var/run/docker.sock for a local setup. +host: + +# Optional proxy URL. +[ proxy_url: ] + +# TLS configuration. +tls_config: + [ ] + +# The port to scrape metrics from, when `role` is nodes, and for discovered +# tasks and services that don't have published ports. +[ port: | default = 80 ] + +# The host to use if the container is in host networking mode. +[ host_networking_host: | default = "localhost" ] + +# Optional filters to limit the discovery process to a subset of available +# resources. +# The available filters are listed in the Docker documentation: +# Containers: https://docs.docker.com/engine/api/v1.41/#operation/ContainerList +[ filters: + [ - name: + values: , [...] ] +] + +# The time after which the containers are refreshed. +[ refresh_interval: | default = 60s ] + +# Authentication information used by Promtail to authenticate itself to the +# Docker daemon. +# Note that `basic_auth` and `authorization` options are mutually exclusive. +# `password` and `password_file` are mutually exclusive. + +# Optional HTTP basic authentication information. +basic_auth: + [ username: ] + [ password: ] + [ password_file: ] + +# Optional `Authorization` header configuration. +authorization: + # Sets the authentication type. + [ type: | default: Bearer ] + # Sets the credentials. It is mutually exclusive with + # `credentials_file`. + [ credentials: ] + # Sets the credentials to the credentials read from the configured file. + # It is mutually exclusive with `credentials`. + [ credentials_file: ] + +# Optional OAuth 2.0 configuration. +# Cannot be used at the same time as basic_auth or authorization. +oauth2: + [ ] + +# Configure whether HTTP requests follow HTTP 3xx redirects. +[ follow_redirects: | default = true ] +``` + +Available meta labels: + + * `__meta_docker_container_id`: the ID of the container + * `__meta_docker_container_name`: the name of the container + * `__meta_docker_container_network_mode`: the network mode of the container + * `__meta_docker_container_label_`: each label of the container + * `__meta_docker_container_log_stream`: the log stream type `stdout` or `stderr` + * `__meta_docker_network_id`: the ID of the network + * `__meta_docker_network_name`: the name of the network + * `__meta_docker_network_ingress`: whether the network is ingress + * `__meta_docker_network_internal`: whether the network is internal + * `__meta_docker_network_label_`: each label of the network + * `__meta_docker_network_scope`: the scope of the network + * `__meta_docker_network_ip`: the IP of the container in this network + * `__meta_docker_port_private`: the port on the container + * `__meta_docker_port_public`: the external port if a port-mapping exists + * `__meta_docker_port_public_ip`: the public IP if a port-mapping exists + +These labels can be used during relabeling. For instance, the following configuration scrapes the container named `flog` and removes the leading slash (`/`) from the container name. + +```yaml +scrape_configs: + - job_name: flog_scrape + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + filters: + - name: name + values: [flog] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' +``` + + ## target_config The `target_config` block controls the behavior of reading files from discovered diff --git a/vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go b/vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go new file mode 100644 index 0000000000..8f6e0a737a --- /dev/null +++ b/vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go @@ -0,0 +1,190 @@ +package stdcopy // import "github.com/docker/docker/pkg/stdcopy" + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "sync" +) + +// StdType is the type of standard stream +// a writer can multiplex to. +type StdType byte + +const ( + // Stdin represents standard input stream type. + Stdin StdType = iota + // Stdout represents standard output stream type. + Stdout + // Stderr represents standard error steam type. + Stderr + // Systemerr represents errors originating from the system that make it + // into the multiplexed stream. + Systemerr + + stdWriterPrefixLen = 8 + stdWriterFdIndex = 0 + stdWriterSizeIndex = 4 + + startingBufLen = 32*1024 + stdWriterPrefixLen + 1 +) + +var bufPool = &sync.Pool{New: func() interface{} { return bytes.NewBuffer(nil) }} + +// stdWriter is wrapper of io.Writer with extra customized info. +type stdWriter struct { + io.Writer + prefix byte +} + +// Write sends the buffer to the underneath writer. +// It inserts the prefix header before the buffer, +// so stdcopy.StdCopy knows where to multiplex the output. +// It makes stdWriter to implement io.Writer. +func (w *stdWriter) Write(p []byte) (n int, err error) { + if w == nil || w.Writer == nil { + return 0, errors.New("Writer not instantiated") + } + if p == nil { + return 0, nil + } + + header := [stdWriterPrefixLen]byte{stdWriterFdIndex: w.prefix} + binary.BigEndian.PutUint32(header[stdWriterSizeIndex:], uint32(len(p))) + buf := bufPool.Get().(*bytes.Buffer) + buf.Write(header[:]) + buf.Write(p) + + n, err = w.Writer.Write(buf.Bytes()) + n -= stdWriterPrefixLen + if n < 0 { + n = 0 + } + + buf.Reset() + bufPool.Put(buf) + return +} + +// NewStdWriter instantiates a new Writer. +// Everything written to it will be encapsulated using a custom format, +// and written to the underlying `w` stream. +// This allows multiple write streams (e.g. stdout and stderr) to be muxed into a single connection. +// `t` indicates the id of the stream to encapsulate. +// It can be stdcopy.Stdin, stdcopy.Stdout, stdcopy.Stderr. +func NewStdWriter(w io.Writer, t StdType) io.Writer { + return &stdWriter{ + Writer: w, + prefix: byte(t), + } +} + +// StdCopy is a modified version of io.Copy. +// +// StdCopy will demultiplex `src`, assuming that it contains two streams, +// previously multiplexed together using a StdWriter instance. +// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`. +// +// StdCopy will read until it hits EOF on `src`. It will then return a nil error. +// In other words: if `err` is non nil, it indicates a real underlying error. +// +// `written` will hold the total number of bytes written to `dstout` and `dsterr`. +func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, err error) { + var ( + buf = make([]byte, startingBufLen) + bufLen = len(buf) + nr, nw int + er, ew error + out io.Writer + frameSize int + ) + + for { + // Make sure we have at least a full header + for nr < stdWriterPrefixLen { + var nr2 int + nr2, er = src.Read(buf[nr:]) + nr += nr2 + if er == io.EOF { + if nr < stdWriterPrefixLen { + return written, nil + } + break + } + if er != nil { + return 0, er + } + } + + stream := StdType(buf[stdWriterFdIndex]) + // Check the first byte to know where to write + switch stream { + case Stdin: + fallthrough + case Stdout: + // Write on stdout + out = dstout + case Stderr: + // Write on stderr + out = dsterr + case Systemerr: + // If we're on Systemerr, we won't write anywhere. + // NB: if this code changes later, make sure you don't try to write + // to outstream if Systemerr is the stream + out = nil + default: + return 0, fmt.Errorf("Unrecognized input header: %d", buf[stdWriterFdIndex]) + } + + // Retrieve the size of the frame + frameSize = int(binary.BigEndian.Uint32(buf[stdWriterSizeIndex : stdWriterSizeIndex+4])) + + // Check if the buffer is big enough to read the frame. + // Extend it if necessary. + if frameSize+stdWriterPrefixLen > bufLen { + buf = append(buf, make([]byte, frameSize+stdWriterPrefixLen-bufLen+1)...) + bufLen = len(buf) + } + + // While the amount of bytes read is less than the size of the frame + header, we keep reading + for nr < frameSize+stdWriterPrefixLen { + var nr2 int + nr2, er = src.Read(buf[nr:]) + nr += nr2 + if er == io.EOF { + if nr < frameSize+stdWriterPrefixLen { + return written, nil + } + break + } + if er != nil { + return 0, er + } + } + + // we might have an error from the source mixed up in our multiplexed + // stream. if we do, return it. + if stream == Systemerr { + return written, fmt.Errorf("error from daemon in stream: %s", string(buf[stdWriterPrefixLen:frameSize+stdWriterPrefixLen])) + } + + // Write the retrieved frame (without header) + nw, ew = out.Write(buf[stdWriterPrefixLen : frameSize+stdWriterPrefixLen]) + if ew != nil { + return 0, ew + } + + // If the frame has not been fully written: error + if nw != frameSize { + return 0, io.ErrShortWrite + } + written += int64(nw) + + // Move the rest of the buffer to the beginning + copy(buf, buf[frameSize+stdWriterPrefixLen:]) + // Move the index + nr -= frameSize + stdWriterPrefixLen + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ca56e5adc7..4c867f5642 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -341,6 +341,7 @@ github.com/docker/docker/pkg/plugins/transport github.com/docker/docker/pkg/pools github.com/docker/docker/pkg/progress github.com/docker/docker/pkg/pubsub +github.com/docker/docker/pkg/stdcopy github.com/docker/docker/pkg/streamformatter github.com/docker/docker/pkg/stringid github.com/docker/docker/pkg/tailfile