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

     1  //go:build linux && cgo
     2  // +build linux,cgo
     3  
     4  package journal
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"strings"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/coreos/go-systemd/sdjournal"
    15  	"github.com/go-kit/log"
    16  	"github.com/go-kit/log/level"
    17  	jsoniter "github.com/json-iterator/go"
    18  	"github.com/pkg/errors"
    19  	"github.com/prometheus/common/model"
    20  	"github.com/prometheus/prometheus/model/labels"
    21  	"github.com/prometheus/prometheus/model/relabel"
    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/scrapeconfig"
    26  	"github.com/grafana/loki/clients/pkg/promtail/targets/target"
    27  
    28  	"github.com/grafana/loki/pkg/logproto"
    29  )
    30  
    31  const (
    32  	// journalEmptyStr is represented as a single-character space because
    33  	// returning an empty string from sdjournal.JournalReaderConfig's
    34  	// Formatter causes an immediate EOF and induces performance issues
    35  	// with how that is handled in sdjournal.
    36  	journalEmptyStr = " "
    37  
    38  	// journalDefaultMaxAgeTime represents the default earliest entry that
    39  	// will be read by the journal reader if there is no saved position
    40  	// newer than the "max_age" time.
    41  	journalDefaultMaxAgeTime = time.Hour * 7
    42  )
    43  
    44  type journalReader interface {
    45  	io.Closer
    46  	Follow(until <-chan time.Time, writer io.Writer) error
    47  }
    48  
    49  // Abstracted functions for interacting with the journal, used for mocking in tests:
    50  type (
    51  	journalReaderFunc func(sdjournal.JournalReaderConfig) (journalReader, error)
    52  	journalEntryFunc  func(cfg sdjournal.JournalReaderConfig, cursor string) (*sdjournal.JournalEntry, error)
    53  )
    54  
    55  // Default implementations of abstracted functions:
    56  var defaultJournalReaderFunc = func(c sdjournal.JournalReaderConfig) (journalReader, error) {
    57  	return sdjournal.NewJournalReader(c)
    58  }
    59  
    60  var defaultJournalEntryFunc = func(c sdjournal.JournalReaderConfig, cursor string) (*sdjournal.JournalEntry, error) {
    61  	var (
    62  		journal *sdjournal.Journal
    63  		err     error
    64  	)
    65  
    66  	if c.Path != "" {
    67  		journal, err = sdjournal.NewJournalFromDir(c.Path)
    68  	} else {
    69  		journal, err = sdjournal.NewJournal()
    70  	}
    71  
    72  	if err != nil {
    73  		return nil, err
    74  	} else if err := journal.SeekCursor(cursor); err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	// Just seeking the cursor won't give us the entry. We should call Next() or Previous()
    79  	// to get the closest following or the closest preceding entry. We have chosen here to call Next(),
    80  	// reason being, if we call Previous() we would re read an already read entry.
    81  	// More info here https://www.freedesktop.org/software/systemd/man/sd_journal_seek_cursor.html#
    82  	_, err = journal.Next()
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	return journal.GetEntry()
    88  }
    89  
    90  // JournalTarget tails systemd journal entries.
    91  // nolint
    92  type JournalTarget struct {
    93  	metrics       *Metrics
    94  	logger        log.Logger
    95  	handler       api.EntryHandler
    96  	positions     positions.Positions
    97  	positionPath  string
    98  	relabelConfig []*relabel.Config
    99  	config        *scrapeconfig.JournalTargetConfig
   100  	labels        model.LabelSet
   101  
   102  	r     journalReader
   103  	until chan time.Time
   104  }
   105  
   106  // NewJournalTarget configures a new JournalTarget.
   107  func NewJournalTarget(
   108  	metrics *Metrics,
   109  	logger log.Logger,
   110  	handler api.EntryHandler,
   111  	positions positions.Positions,
   112  	jobName string,
   113  	relabelConfig []*relabel.Config,
   114  	targetConfig *scrapeconfig.JournalTargetConfig,
   115  ) (*JournalTarget, error) {
   116  
   117  	return journalTargetWithReader(
   118  		metrics,
   119  		logger,
   120  		handler,
   121  		positions,
   122  		jobName,
   123  		relabelConfig,
   124  		targetConfig,
   125  		defaultJournalReaderFunc,
   126  		defaultJournalEntryFunc,
   127  	)
   128  }
   129  
   130  func journalTargetWithReader(
   131  	metrics *Metrics,
   132  	logger log.Logger,
   133  	handler api.EntryHandler,
   134  	pos positions.Positions,
   135  	jobName string,
   136  	relabelConfig []*relabel.Config,
   137  	targetConfig *scrapeconfig.JournalTargetConfig,
   138  	readerFunc journalReaderFunc,
   139  	entryFunc journalEntryFunc,
   140  ) (*JournalTarget, error) {
   141  
   142  	positionPath := positions.CursorKey(jobName)
   143  	position := pos.GetString(positionPath)
   144  
   145  	if readerFunc == nil {
   146  		readerFunc = defaultJournalReaderFunc
   147  	}
   148  	if entryFunc == nil {
   149  		entryFunc = defaultJournalEntryFunc
   150  	}
   151  
   152  	until := make(chan time.Time)
   153  	t := &JournalTarget{
   154  		metrics:       metrics,
   155  		logger:        logger,
   156  		handler:       handler,
   157  		positions:     pos,
   158  		positionPath:  positionPath,
   159  		relabelConfig: relabelConfig,
   160  		labels:        targetConfig.Labels,
   161  		config:        targetConfig,
   162  
   163  		until: until,
   164  	}
   165  
   166  	var maxAge time.Duration
   167  	var err error
   168  	if targetConfig.MaxAge == "" {
   169  		maxAge = journalDefaultMaxAgeTime
   170  	} else {
   171  		maxAge, err = time.ParseDuration(targetConfig.MaxAge)
   172  	}
   173  	if err != nil {
   174  		return nil, errors.Wrap(err, "parsing journal reader 'max_age' config value")
   175  	}
   176  
   177  	cfg := t.generateJournalConfig(journalConfigBuilder{
   178  		JournalPath: targetConfig.Path,
   179  		Position:    position,
   180  		MaxAge:      maxAge,
   181  		EntryFunc:   entryFunc,
   182  	})
   183  	t.r, err = readerFunc(cfg)
   184  	if err != nil {
   185  		return nil, errors.Wrap(err, "creating journal reader")
   186  	}
   187  
   188  	go func() {
   189  		for {
   190  			err := t.r.Follow(until, ioutil.Discard)
   191  			if err != nil {
   192  				level.Error(t.logger).Log("msg", "received error during sdjournal follow", "err", err.Error())
   193  
   194  				if err == sdjournal.ErrExpired || err == syscall.EBADMSG || err == io.EOF {
   195  					level.Error(t.logger).Log("msg", "unable to follow journal", "err", err.Error())
   196  					return
   197  				}
   198  			}
   199  
   200  			// prevent tight loop
   201  			time.Sleep(100 * time.Millisecond)
   202  		}
   203  	}()
   204  
   205  	return t, nil
   206  }
   207  
   208  type journalConfigBuilder struct {
   209  	JournalPath string
   210  	Position    string
   211  	MaxAge      time.Duration
   212  	EntryFunc   journalEntryFunc
   213  }
   214  
   215  // generateJournalConfig generates a journal config by trying to intelligently
   216  // determine if a time offset or the cursor should be used for the starting
   217  // position in the reader.
   218  func (t *JournalTarget) generateJournalConfig(
   219  	cb journalConfigBuilder,
   220  ) sdjournal.JournalReaderConfig {
   221  
   222  	cfg := sdjournal.JournalReaderConfig{
   223  		Path:      cb.JournalPath,
   224  		Formatter: t.formatter,
   225  	}
   226  
   227  	// When generating the JournalReaderConfig, we want to preferably
   228  	// use the Cursor, since it's guaranteed unique to a given journal
   229  	// entry. When we don't know the cursor position (or want to set
   230  	// a start time), we'll fall back to the less-precise Since, which
   231  	// takes a negative duration back from the current system time.
   232  	//
   233  	// The presence of Since takes precedence over Cursor, so we only
   234  	// ever set one and not both here.
   235  
   236  	if cb.Position == "" {
   237  		cfg.Since = -1 * cb.MaxAge
   238  		return cfg
   239  	}
   240  
   241  	// We have a saved position and need to get that entry to see if it's
   242  	// older than cb.MaxAge. If it _is_ older, then we need to use cfg.Since
   243  	// rather than cfg.Cursor.
   244  	entry, err := cb.EntryFunc(cfg, cb.Position)
   245  	if err != nil {
   246  		level.Error(t.logger).Log("msg", "received error reading saved journal position", "err", err.Error())
   247  		cfg.Since = -1 * cb.MaxAge
   248  		return cfg
   249  	}
   250  
   251  	ts := time.Unix(0, int64(entry.RealtimeTimestamp)*int64(time.Microsecond))
   252  	if time.Since(ts) > cb.MaxAge {
   253  		cfg.Since = -1 * cb.MaxAge
   254  		return cfg
   255  	}
   256  
   257  	cfg.Cursor = cb.Position
   258  	return cfg
   259  }
   260  
   261  func (t *JournalTarget) formatter(entry *sdjournal.JournalEntry) (string, error) {
   262  	ts := time.Unix(0, int64(entry.RealtimeTimestamp)*int64(time.Microsecond))
   263  
   264  	var msg string
   265  
   266  	if t.config.JSON {
   267  		json := jsoniter.ConfigCompatibleWithStandardLibrary
   268  
   269  		bb, err := json.Marshal(entry.Fields)
   270  		if err != nil {
   271  			level.Error(t.logger).Log("msg", "could not marshal journal fields to JSON", "err", err, "unit", entry.Fields["_SYSTEMD_UNIT"])
   272  			return journalEmptyStr, nil
   273  		}
   274  		msg = string(bb)
   275  	} else {
   276  		var ok bool
   277  		msg, ok = entry.Fields["MESSAGE"]
   278  		if !ok {
   279  			level.Debug(t.logger).Log("msg", "received journal entry with no MESSAGE field", "unit", entry.Fields["_SYSTEMD_UNIT"])
   280  			t.metrics.journalErrors.WithLabelValues(noMessageError).Inc()
   281  			return journalEmptyStr, nil
   282  		}
   283  	}
   284  
   285  	entryLabels := makeJournalFields(entry.Fields)
   286  
   287  	// Add constant labels
   288  	for k, v := range t.labels {
   289  		entryLabels[string(k)] = string(v)
   290  	}
   291  
   292  	processedLabels := relabel.Process(labels.FromMap(entryLabels), t.relabelConfig...)
   293  
   294  	processedLabelsMap := processedLabels.Map()
   295  	labels := make(model.LabelSet, len(processedLabelsMap))
   296  	for k, v := range processedLabelsMap {
   297  		if k[0:2] == "__" {
   298  			continue
   299  		}
   300  
   301  		labels[model.LabelName(k)] = model.LabelValue(v)
   302  	}
   303  	if len(labels) == 0 {
   304  		// No labels, drop journal entry
   305  		level.Debug(t.logger).Log("msg", "received journal entry with no labels", "unit", entry.Fields["_SYSTEMD_UNIT"])
   306  		t.metrics.journalErrors.WithLabelValues(emptyLabelsError).Inc()
   307  		return journalEmptyStr, nil
   308  	}
   309  
   310  	t.metrics.journalLines.Inc()
   311  	t.positions.PutString(t.positionPath, entry.Cursor)
   312  	t.handler.Chan() <- api.Entry{
   313  		Labels: labels,
   314  		Entry: logproto.Entry{
   315  			Line:      msg,
   316  			Timestamp: ts,
   317  		},
   318  	}
   319  	return journalEmptyStr, nil
   320  }
   321  
   322  // Type returns JournalTargetType.
   323  func (t *JournalTarget) Type() target.TargetType {
   324  	return target.JournalTargetType
   325  }
   326  
   327  // Ready indicates whether or not the journal is ready to be
   328  // read from.
   329  func (t *JournalTarget) Ready() bool {
   330  	return true
   331  }
   332  
   333  // DiscoveredLabels returns the set of labels discovered by
   334  // the JournalTarget, which is always nil. Implements
   335  // Target.
   336  func (t *JournalTarget) DiscoveredLabels() model.LabelSet {
   337  	return nil
   338  }
   339  
   340  // Labels returns the set of labels that statically apply to
   341  // all log entries produced by the JournalTarget.
   342  func (t *JournalTarget) Labels() model.LabelSet {
   343  	return t.labels
   344  }
   345  
   346  // Details returns target-specific details.
   347  func (t *JournalTarget) Details() interface{} {
   348  	return map[string]string{
   349  		"position": t.positions.GetString(t.positionPath),
   350  	}
   351  }
   352  
   353  // Stop shuts down the JournalTarget.
   354  func (t *JournalTarget) Stop() error {
   355  	t.until <- time.Now()
   356  	err := t.r.Close()
   357  	t.handler.Stop()
   358  	return err
   359  }
   360  
   361  func makeJournalFields(fields map[string]string) map[string]string {
   362  	result := make(map[string]string, len(fields))
   363  	for k, v := range fields {
   364  		if k == "PRIORITY" {
   365  			result[fmt.Sprintf("__journal_%s_%s", strings.ToLower(k), "keyword")] = makeJournalPriority(v)
   366  		}
   367  		result[fmt.Sprintf("__journal_%s", strings.ToLower(k))] = v
   368  	}
   369  	return result
   370  }
   371  
   372  func makeJournalPriority(priority string) string {
   373  	switch priority {
   374  	case "0":
   375  		return "emerg"
   376  	case "1":
   377  		return "alert"
   378  	case "2":
   379  		return "crit"
   380  	case "3":
   381  		return "error"
   382  	case "4":
   383  		return "warning"
   384  	case "5":
   385  		return "notice"
   386  	case "6":
   387  		return "info"
   388  	case "7":
   389  		return "debug"
   390  	}
   391  	return priority
   392  }