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

     1  //go:build linux && cgo
     2  // +build linux,cgo
     3  
     4  package journal
     5  
     6  import (
     7  	"io"
     8  	"os"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/coreos/go-systemd/sdjournal"
    14  	"github.com/go-kit/log"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/prometheus/client_golang/prometheus/testutil"
    17  	"github.com/prometheus/prometheus/model/relabel"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  	"gopkg.in/yaml.v2"
    21  
    22  	"github.com/grafana/loki/clients/pkg/promtail/client/fake"
    23  	"github.com/grafana/loki/clients/pkg/promtail/positions"
    24  	"github.com/grafana/loki/clients/pkg/promtail/scrapeconfig"
    25  	"github.com/grafana/loki/clients/pkg/promtail/targets/testutils"
    26  )
    27  
    28  type mockJournalReader struct {
    29  	config sdjournal.JournalReaderConfig
    30  	t      *testing.T
    31  }
    32  
    33  func newMockJournalReader(c sdjournal.JournalReaderConfig) (journalReader, error) {
    34  	return &mockJournalReader{config: c}, nil
    35  }
    36  
    37  func (r *mockJournalReader) Close() error {
    38  	return nil
    39  }
    40  
    41  func (r *mockJournalReader) Follow(until <-chan time.Time, writer io.Writer) error {
    42  	<-until
    43  	return nil
    44  }
    45  
    46  func newMockJournalEntry(entry *sdjournal.JournalEntry) journalEntryFunc {
    47  	return func(c sdjournal.JournalReaderConfig, cursor string) (*sdjournal.JournalEntry, error) {
    48  		return entry, nil
    49  	}
    50  }
    51  
    52  func (r *mockJournalReader) Write(fields map[string]string) {
    53  	allFields := make(map[string]string, len(fields))
    54  	for k, v := range fields {
    55  		allFields[k] = v
    56  	}
    57  
    58  	ts := uint64(time.Now().UnixNano())
    59  
    60  	_, err := r.config.Formatter(&sdjournal.JournalEntry{
    61  		Fields:             allFields,
    62  		MonotonicTimestamp: ts,
    63  		RealtimeTimestamp:  ts,
    64  	})
    65  	assert.NoError(r.t, err)
    66  }
    67  
    68  func TestJournalTarget(t *testing.T) {
    69  	w := log.NewSyncWriter(os.Stderr)
    70  	logger := log.NewLogfmtLogger(w)
    71  
    72  	testutils.InitRandom()
    73  	dirName := "/tmp/" + testutils.RandName()
    74  	positionsFileName := dirName + "/positions.yml"
    75  
    76  	// Set the sync period to a really long value, to guarantee the sync timer
    77  	// never runs, this way we know everything saved was done through channel
    78  	// notifications when target.stop() was called.
    79  	ps, err := positions.New(logger, positions.Config{
    80  		SyncPeriod:    10 * time.Second,
    81  		PositionsFile: positionsFileName,
    82  	})
    83  	if err != nil {
    84  		t.Fatal(err)
    85  	}
    86  
    87  	client := fake.New(func() {})
    88  
    89  	relabelCfg := `
    90  - source_labels: ['__journal_code_file']
    91    regex: 'journaltarget_test\.go'
    92    action: 'keep'
    93  - source_labels: ['__journal_code_file']
    94    target_label: 'code_file'`
    95  
    96  	var relabels []*relabel.Config
    97  	err = yaml.Unmarshal([]byte(relabelCfg), &relabels)
    98  	require.NoError(t, err)
    99  
   100  	registry := prometheus.NewRegistry()
   101  	jt, err := journalTargetWithReader(NewMetrics(registry), logger, client, ps, "test", relabels,
   102  		&scrapeconfig.JournalTargetConfig{}, newMockJournalReader, newMockJournalEntry(nil))
   103  	require.NoError(t, err)
   104  
   105  	r := jt.r.(*mockJournalReader)
   106  	r.t = t
   107  
   108  	for i := 0; i < 10; i++ {
   109  		r.Write(map[string]string{
   110  			"MESSAGE":   "ping",
   111  			"CODE_FILE": "journaltarget_test.go",
   112  		})
   113  		assert.NoError(t, err)
   114  	}
   115  	require.NoError(t, jt.Stop())
   116  	client.Stop()
   117  
   118  	expectedMetrics := `# HELP promtail_journal_target_lines_total Total number of successful journal lines read
   119  	# TYPE promtail_journal_target_lines_total counter
   120  	promtail_journal_target_lines_total 10
   121  	`
   122  
   123  	if err := testutil.GatherAndCompare(registry,
   124  		strings.NewReader(expectedMetrics)); err != nil {
   125  		t.Fatalf("mismatch metrics: %v", err)
   126  	}
   127  	assert.Len(t, client.Received(), 10)
   128  }
   129  
   130  func TestJournalTargetParsingErrors(t *testing.T) {
   131  	w := log.NewSyncWriter(os.Stderr)
   132  	logger := log.NewLogfmtLogger(w)
   133  
   134  	testutils.InitRandom()
   135  	dirName := "/tmp/" + testutils.RandName()
   136  	positionsFileName := dirName + "/positions.yml"
   137  
   138  	// Set the sync period to a really long value, to guarantee the sync timer
   139  	// never runs, this way we know everything saved was done through channel
   140  	// notifications when target.stop() was called.
   141  	ps, err := positions.New(logger, positions.Config{
   142  		SyncPeriod:    10 * time.Second,
   143  		PositionsFile: positionsFileName,
   144  	})
   145  	if err != nil {
   146  		t.Fatal(err)
   147  	}
   148  
   149  	client := fake.New(func() {})
   150  
   151  	// We specify no relabel rules, so that we end up with an empty labelset
   152  	var relabels []*relabel.Config
   153  
   154  	registry := prometheus.NewRegistry()
   155  	jt, err := journalTargetWithReader(NewMetrics(registry), logger, client, ps, "test", relabels,
   156  		&scrapeconfig.JournalTargetConfig{}, newMockJournalReader, newMockJournalEntry(nil))
   157  	require.NoError(t, err)
   158  
   159  	r := jt.r.(*mockJournalReader)
   160  	r.t = t
   161  
   162  	// No labels but correct message
   163  	for i := 0; i < 10; i++ {
   164  		r.Write(map[string]string{
   165  			"MESSAGE":   "ping",
   166  			"CODE_FILE": "journaltarget_test.go",
   167  		})
   168  		assert.NoError(t, err)
   169  	}
   170  
   171  	// No labels and no message
   172  	for i := 0; i < 10; i++ {
   173  		r.Write(map[string]string{
   174  			"CODE_FILE": "journaltarget_test.go",
   175  		})
   176  		assert.NoError(t, err)
   177  	}
   178  	require.NoError(t, jt.Stop())
   179  	client.Stop()
   180  
   181  	expectedMetrics := `# HELP promtail_journal_target_lines_total Total number of successful journal lines read
   182  	# TYPE promtail_journal_target_lines_total counter
   183  	promtail_journal_target_lines_total 0
   184  	# HELP promtail_journal_target_parsing_errors_total Total number of parsing errors while reading journal messages
   185  	# TYPE promtail_journal_target_parsing_errors_total counter
   186  	promtail_journal_target_parsing_errors_total{error="empty_labels"} 10
   187  	promtail_journal_target_parsing_errors_total{error="no_message"} 10
   188  	`
   189  
   190  	if err := testutil.GatherAndCompare(registry,
   191  		strings.NewReader(expectedMetrics)); err != nil {
   192  		t.Fatalf("mismatch metrics: %v", err)
   193  	}
   194  
   195  	assert.Len(t, client.Received(), 0)
   196  }
   197  
   198  func TestJournalTarget_JSON(t *testing.T) {
   199  	w := log.NewSyncWriter(os.Stderr)
   200  	logger := log.NewLogfmtLogger(w)
   201  
   202  	testutils.InitRandom()
   203  	dirName := "/tmp/" + testutils.RandName()
   204  	positionsFileName := dirName + "/positions.yml"
   205  
   206  	// Set the sync period to a really long value, to guarantee the sync timer
   207  	// never runs, this way we know everything saved was done through channel
   208  	// notifications when target.stop() was called.
   209  	ps, err := positions.New(logger, positions.Config{
   210  		SyncPeriod:    10 * time.Second,
   211  		PositionsFile: positionsFileName,
   212  	})
   213  	if err != nil {
   214  		t.Fatal(err)
   215  	}
   216  
   217  	client := fake.New(func() {})
   218  
   219  	relabelCfg := `
   220  - source_labels: ['__journal_code_file']
   221    regex: 'journaltarget_test\.go'
   222    action: 'keep'
   223  - source_labels: ['__journal_code_file']
   224    target_label: 'code_file'`
   225  
   226  	var relabels []*relabel.Config
   227  	err = yaml.Unmarshal([]byte(relabelCfg), &relabels)
   228  	require.NoError(t, err)
   229  
   230  	cfg := &scrapeconfig.JournalTargetConfig{JSON: true}
   231  
   232  	jt, err := journalTargetWithReader(NewMetrics(prometheus.NewRegistry()), logger, client, ps, "test", relabels,
   233  		cfg, newMockJournalReader, newMockJournalEntry(nil))
   234  	require.NoError(t, err)
   235  
   236  	r := jt.r.(*mockJournalReader)
   237  	r.t = t
   238  
   239  	for i := 0; i < 10; i++ {
   240  		r.Write(map[string]string{
   241  			"MESSAGE":     "ping",
   242  			"CODE_FILE":   "journaltarget_test.go",
   243  			"OTHER_FIELD": "foobar",
   244  		})
   245  		assert.NoError(t, err)
   246  
   247  	}
   248  	expectMsg := `{"CODE_FILE":"journaltarget_test.go","MESSAGE":"ping","OTHER_FIELD":"foobar"}`
   249  	require.NoError(t, jt.Stop())
   250  	client.Stop()
   251  
   252  	assert.Len(t, client.Received(), 10)
   253  	for i := 0; i < 10; i++ {
   254  		require.Equal(t, expectMsg, client.Received()[i].Line)
   255  	}
   256  }
   257  
   258  func TestJournalTarget_Since(t *testing.T) {
   259  	w := log.NewSyncWriter(os.Stderr)
   260  	logger := log.NewLogfmtLogger(w)
   261  
   262  	testutils.InitRandom()
   263  	dirName := "/tmp/" + testutils.RandName()
   264  	positionsFileName := dirName + "/positions.yml"
   265  
   266  	// Set the sync period to a really long value, to guarantee the sync timer
   267  	// never runs, this way we know everything saved was done through channel
   268  	// notifications when target.stop() was called.
   269  	ps, err := positions.New(logger, positions.Config{
   270  		SyncPeriod:    10 * time.Second,
   271  		PositionsFile: positionsFileName,
   272  	})
   273  	if err != nil {
   274  		t.Fatal(err)
   275  	}
   276  
   277  	client := fake.New(func() {})
   278  
   279  	cfg := scrapeconfig.JournalTargetConfig{
   280  		MaxAge: "4h",
   281  	}
   282  
   283  	jt, err := journalTargetWithReader(NewMetrics(prometheus.NewRegistry()), logger, client, ps, "test", nil,
   284  		&cfg, newMockJournalReader, newMockJournalEntry(nil))
   285  	require.NoError(t, err)
   286  
   287  	r := jt.r.(*mockJournalReader)
   288  	require.Equal(t, r.config.Since, -1*time.Hour*4)
   289  	client.Stop()
   290  }
   291  
   292  func TestJournalTarget_Cursor_TooOld(t *testing.T) {
   293  	w := log.NewSyncWriter(os.Stderr)
   294  	logger := log.NewLogfmtLogger(w)
   295  
   296  	testutils.InitRandom()
   297  	dirName := "/tmp/" + testutils.RandName()
   298  	positionsFileName := dirName + "/positions.yml"
   299  
   300  	// Set the sync period to a really long value, to guarantee the sync timer
   301  	// never runs, this way we know everything saved was done through channel
   302  	// notifications when target.stop() was called.
   303  	ps, err := positions.New(logger, positions.Config{
   304  		SyncPeriod:    10 * time.Second,
   305  		PositionsFile: positionsFileName,
   306  	})
   307  	if err != nil {
   308  		t.Fatal(err)
   309  	}
   310  	ps.PutString("journal-test", "foobar")
   311  
   312  	client := fake.New(func() {})
   313  
   314  	cfg := scrapeconfig.JournalTargetConfig{}
   315  
   316  	entryTs := time.Date(1980, time.July, 3, 12, 0, 0, 0, time.UTC)
   317  	journalEntry := newMockJournalEntry(&sdjournal.JournalEntry{
   318  		Cursor:            "foobar",
   319  		Fields:            nil,
   320  		RealtimeTimestamp: uint64(entryTs.UnixNano()),
   321  	})
   322  
   323  	jt, err := journalTargetWithReader(NewMetrics(prometheus.NewRegistry()), logger, client, ps, "test", nil,
   324  		&cfg, newMockJournalReader, journalEntry)
   325  	require.NoError(t, err)
   326  
   327  	r := jt.r.(*mockJournalReader)
   328  	require.Equal(t, r.config.Since, -1*time.Hour*7)
   329  	client.Stop()
   330  }
   331  
   332  func TestJournalTarget_Cursor_NotTooOld(t *testing.T) {
   333  	w := log.NewSyncWriter(os.Stderr)
   334  	logger := log.NewLogfmtLogger(w)
   335  
   336  	testutils.InitRandom()
   337  	dirName := "/tmp/" + testutils.RandName()
   338  	positionsFileName := dirName + "/positions.yml"
   339  
   340  	// Set the sync period to a really long value, to guarantee the sync timer
   341  	// never runs, this way we know everything saved was done through channel
   342  	// notifications when target.stop() was called.
   343  	ps, err := positions.New(logger, positions.Config{
   344  		SyncPeriod:    10 * time.Second,
   345  		PositionsFile: positionsFileName,
   346  	})
   347  	if err != nil {
   348  		t.Fatal(err)
   349  	}
   350  	ps.PutString(positions.CursorKey("test"), "foobar")
   351  
   352  	client := fake.New(func() {})
   353  
   354  	cfg := scrapeconfig.JournalTargetConfig{}
   355  
   356  	entryTs := time.Now().Add(-time.Hour)
   357  	journalEntry := newMockJournalEntry(&sdjournal.JournalEntry{
   358  		Cursor:            "foobar",
   359  		Fields:            nil,
   360  		RealtimeTimestamp: uint64(entryTs.UnixNano() / int64(time.Microsecond)),
   361  	})
   362  
   363  	jt, err := journalTargetWithReader(NewMetrics(prometheus.NewRegistry()), logger, client, ps, "test", nil,
   364  		&cfg, newMockJournalReader, journalEntry)
   365  	require.NoError(t, err)
   366  
   367  	r := jt.r.(*mockJournalReader)
   368  	require.Equal(t, r.config.Since, time.Duration(0))
   369  	require.Equal(t, r.config.Cursor, "foobar")
   370  	client.Stop()
   371  }
   372  
   373  func Test_MakeJournalFields(t *testing.T) {
   374  	entryFields := map[string]string{
   375  		"CODE_FILE":   "journaltarget_test.go",
   376  		"OTHER_FIELD": "foobar",
   377  		"PRIORITY":    "6",
   378  	}
   379  	receivedFields := makeJournalFields(entryFields)
   380  	expectedFields := map[string]string{
   381  		"__journal_code_file":        "journaltarget_test.go",
   382  		"__journal_other_field":      "foobar",
   383  		"__journal_priority":         "6",
   384  		"__journal_priority_keyword": "info",
   385  	}
   386  	assert.Equal(t, expectedFields, receivedFields)
   387  }