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 }