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 }