github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/targets/docker/target.go (about)

     1  package docker
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	docker_types "github.com/docker/docker/api/types"
    14  	"github.com/docker/docker/client"
    15  	"github.com/docker/docker/pkg/stdcopy"
    16  	"github.com/go-kit/log"
    17  	"github.com/go-kit/log/level"
    18  	"github.com/prometheus/common/model"
    19  	"github.com/prometheus/prometheus/model/labels"
    20  	"github.com/prometheus/prometheus/model/relabel"
    21  	"go.uber.org/atomic"
    22  
    23  	"github.com/grafana/loki/clients/pkg/promtail/api"
    24  	"github.com/grafana/loki/clients/pkg/promtail/positions"
    25  	"github.com/grafana/loki/clients/pkg/promtail/targets/target"
    26  
    27  	"github.com/grafana/loki/pkg/logproto"
    28  )
    29  
    30  type Target struct {
    31  	logger        log.Logger
    32  	handler       api.EntryHandler
    33  	since         int64
    34  	positions     positions.Positions
    35  	containerName string
    36  	labels        model.LabelSet
    37  	relabelConfig []*relabel.Config
    38  	metrics       *Metrics
    39  
    40  	cancel  context.CancelFunc
    41  	client  client.APIClient
    42  	wg      sync.WaitGroup
    43  	running *atomic.Bool
    44  	err     error
    45  }
    46  
    47  func NewTarget(
    48  	metrics *Metrics,
    49  	logger log.Logger,
    50  	handler api.EntryHandler,
    51  	position positions.Positions,
    52  	containerName string,
    53  	labels model.LabelSet,
    54  	relabelConfig []*relabel.Config,
    55  	client client.APIClient,
    56  ) (*Target, error) {
    57  
    58  	pos, err := position.Get(positions.CursorKey(containerName))
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	var since int64
    63  	if pos != 0 {
    64  		since = pos
    65  	}
    66  
    67  	t := &Target{
    68  		logger:        logger,
    69  		handler:       handler,
    70  		since:         since,
    71  		positions:     position,
    72  		containerName: containerName,
    73  		labels:        labels,
    74  		relabelConfig: relabelConfig,
    75  		metrics:       metrics,
    76  
    77  		client:  client,
    78  		running: atomic.NewBool(false),
    79  	}
    80  	t.startIfNotRunning()
    81  	return t, nil
    82  }
    83  
    84  func (t *Target) processLoop(ctx context.Context) {
    85  	t.running.Store(true)
    86  	defer t.running.Store(false)
    87  
    88  	t.wg.Add(1)
    89  	defer t.wg.Done()
    90  
    91  	opts := docker_types.ContainerLogsOptions{
    92  		ShowStdout: true,
    93  		ShowStderr: true,
    94  		Follow:     true,
    95  		Timestamps: true,
    96  		Since:      strconv.FormatInt(t.since, 10),
    97  	}
    98  
    99  	logs, err := t.client.ContainerLogs(ctx, t.containerName, opts)
   100  	if err != nil {
   101  		level.Error(t.logger).Log("msg", "could not fetch logs for container", "container", t.containerName, "err", err)
   102  		t.err = err
   103  		return
   104  	}
   105  
   106  	// Start transferring
   107  	rstdout, wstdout := io.Pipe()
   108  	rstderr, wstderr := io.Pipe()
   109  	t.wg.Add(1)
   110  	go func() {
   111  		defer func() {
   112  			t.wg.Done()
   113  			wstdout.Close()
   114  			wstderr.Close()
   115  			t.Stop()
   116  		}()
   117  
   118  		written, err := stdcopy.StdCopy(wstdout, wstderr, logs)
   119  		if err != nil {
   120  			level.Warn(t.logger).Log("msg", "could not transfer logs", "written", written, "container", t.containerName, "err", err)
   121  		} else {
   122  			level.Info(t.logger).Log("msg", "finished transferring logs", "written", written, "container", t.containerName)
   123  		}
   124  	}()
   125  
   126  	// Start processing
   127  	t.wg.Add(2)
   128  	go t.process(rstdout, "stdout")
   129  	go t.process(rstderr, "stderr")
   130  
   131  	// Wait until done
   132  	<-ctx.Done()
   133  	logs.Close()
   134  	level.Debug(t.logger).Log("msg", "done processing Docker logs", "container", t.containerName)
   135  }
   136  
   137  // extractTs tries for read the timestamp from the beginning of the log line.
   138  // It's expected to follow the format 2006-01-02T15:04:05.999999999Z07:00.
   139  func extractTs(line string) (time.Time, string, error) {
   140  	pair := strings.SplitN(line, " ", 2)
   141  	if len(pair) != 2 {
   142  		return time.Now(), line, fmt.Errorf("Could not find timestamp in '%s'", line)
   143  	}
   144  	ts, err := time.Parse("2006-01-02T15:04:05.999999999Z07:00", pair[0])
   145  	if err != nil {
   146  		return time.Now(), line, fmt.Errorf("Could not parse timestamp from '%s': %w", pair[0], err)
   147  	}
   148  	return ts, pair[1], nil
   149  }
   150  
   151  // https://devmarkpro.com/working-big-files-golang
   152  func readLine(r *bufio.Reader) (string, error) {
   153  	var (
   154  		isPrefix = true
   155  		err      error
   156  		line, ln []byte
   157  	)
   158  
   159  	for isPrefix && err == nil {
   160  		line, isPrefix, err = r.ReadLine()
   161  		ln = append(ln, line...)
   162  	}
   163  
   164  	return string(ln), err
   165  }
   166  
   167  func (t *Target) process(r io.Reader, logStream string) {
   168  	defer func() {
   169  		t.wg.Done()
   170  	}()
   171  
   172  	reader := bufio.NewReader(r)
   173  	for {
   174  		line, err := readLine(reader)
   175  		if err != nil {
   176  			if err == io.EOF {
   177  				break
   178  			}
   179  			level.Error(t.logger).Log("msg", "error reading docker log line, skipping line", "err", err)
   180  			t.metrics.dockerErrors.Inc()
   181  		}
   182  
   183  		ts, line, err := extractTs(line)
   184  		if err != nil {
   185  			level.Error(t.logger).Log("msg", "could not extract timestamp, skipping line", "err", err)
   186  			t.metrics.dockerErrors.Inc()
   187  			continue
   188  		}
   189  
   190  		// Add all labels from the config, relabel and filter them.
   191  		lb := labels.NewBuilder(nil)
   192  		for k, v := range t.labels {
   193  			lb.Set(string(k), string(v))
   194  		}
   195  		lb.Set(dockerLabelLogStream, logStream)
   196  		processed := relabel.Process(lb.Labels(), t.relabelConfig...)
   197  
   198  		filtered := make(model.LabelSet)
   199  		for _, lbl := range processed {
   200  			if strings.HasPrefix(lbl.Name, "__") {
   201  				continue
   202  			}
   203  			filtered[model.LabelName(lbl.Name)] = model.LabelValue(lbl.Value)
   204  		}
   205  
   206  		t.handler.Chan() <- api.Entry{
   207  			Labels: filtered,
   208  			Entry: logproto.Entry{
   209  				Timestamp: ts,
   210  				Line:      line,
   211  			},
   212  		}
   213  		t.metrics.dockerEntries.Inc()
   214  		t.positions.Put(positions.CursorKey(t.containerName), ts.Unix())
   215  	}
   216  }
   217  
   218  // startIfNotRunning starts processing container logs. The operation is idempotent , i.e. the processing cannot be started twice.
   219  func (t *Target) startIfNotRunning() {
   220  	if t.running.CAS(false, true) {
   221  		level.Debug(t.logger).Log("msg", "starting process loop", "container", t.containerName)
   222  		ctx, cancel := context.WithCancel(context.Background())
   223  		t.cancel = cancel
   224  		go t.processLoop(ctx)
   225  	} else {
   226  		level.Debug(t.logger).Log("msg", "attempted to start process loop but it's already running", "container", t.containerName)
   227  	}
   228  }
   229  
   230  func (t *Target) Stop() {
   231  	t.cancel()
   232  	t.wg.Wait()
   233  	level.Debug(t.logger).Log("msg", "stopped Docker target", "container", t.containerName)
   234  }
   235  
   236  func (t *Target) Type() target.TargetType {
   237  	return target.DockerTargetType
   238  }
   239  
   240  func (t *Target) Ready() bool {
   241  	return t.running.Load()
   242  }
   243  
   244  func (t *Target) DiscoveredLabels() model.LabelSet {
   245  	return t.labels
   246  }
   247  
   248  func (t *Target) Labels() model.LabelSet {
   249  	return t.labels
   250  }
   251  
   252  // Details returns target-specific details.
   253  func (t *Target) Details() interface{} {
   254  	return map[string]string{
   255  		"id":       t.containerName,
   256  		"error":    t.err.Error(),
   257  		"position": t.positions.GetString(positions.CursorKey(t.containerName)),
   258  		"running":  strconv.FormatBool(t.running.Load()),
   259  	}
   260  }