github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/targets/file/filetarget.go (about) 1 package file 2 3 import ( 4 "flag" 5 "os" 6 "path/filepath" 7 "time" 8 9 "github.com/bmatcuk/doublestar" 10 "github.com/go-kit/log" 11 "github.com/go-kit/log/level" 12 "github.com/pkg/errors" 13 "github.com/prometheus/client_golang/prometheus" 14 "github.com/prometheus/common/model" 15 fsnotify "gopkg.in/fsnotify.v1" 16 17 "github.com/grafana/loki/clients/pkg/promtail/api" 18 "github.com/grafana/loki/clients/pkg/promtail/client" 19 "github.com/grafana/loki/clients/pkg/promtail/positions" 20 "github.com/grafana/loki/clients/pkg/promtail/targets/target" 21 ) 22 23 const ( 24 FilenameLabel = "filename" 25 ) 26 27 // Config describes behavior for Target 28 type Config struct { 29 SyncPeriod time.Duration `yaml:"sync_period"` 30 Stdin bool `yaml:"stdin"` 31 } 32 33 // RegisterFlags with prefix registers flags where every name is prefixed by 34 // prefix. If prefix is a non-empty string, prefix should end with a period. 35 func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) { 36 f.DurationVar(&cfg.SyncPeriod, prefix+"target.sync-period", 10*time.Second, "Period to resync directories being watched and files being tailed.") 37 f.BoolVar(&cfg.Stdin, prefix+"stdin", false, "Set to true to pipe logs to promtail.") 38 } 39 40 // RegisterFlags register flags. 41 func (cfg *Config) RegisterFlags(flags *flag.FlagSet) { 42 cfg.RegisterFlagsWithPrefix("", flags) 43 } 44 45 type fileTargetEventType string 46 47 const ( 48 fileTargetEventWatchStart fileTargetEventType = "WATCH_START" 49 fileTargetEventWatchStop fileTargetEventType = "WATCH_STOP" 50 ) 51 52 type fileTargetEvent struct { 53 path string 54 eventType fileTargetEventType 55 } 56 57 // FileTarget describes a particular set of logs. 58 // nolint:revive 59 type FileTarget struct { 60 metrics *Metrics 61 logger log.Logger 62 63 handler api.EntryHandler 64 positions positions.Positions 65 labels model.LabelSet 66 discoveredLabels model.LabelSet 67 68 fileEventWatcher chan fsnotify.Event 69 targetEventHandler chan fileTargetEvent 70 watches map[string]struct{} 71 path string 72 pathExclude string 73 quit chan struct{} 74 done chan struct{} 75 76 tails map[string]*tailer 77 78 targetConfig *Config 79 80 encoding string 81 } 82 83 // NewFileTarget create a new FileTarget. 84 func NewFileTarget( 85 metrics *Metrics, 86 logger log.Logger, 87 handler api.EntryHandler, 88 positions positions.Positions, 89 path string, 90 pathExclude string, 91 labels model.LabelSet, 92 discoveredLabels model.LabelSet, 93 targetConfig *Config, 94 fileEventWatcher chan fsnotify.Event, 95 targetEventHandler chan fileTargetEvent, 96 encoding string, 97 ) (*FileTarget, error) { 98 t := &FileTarget{ 99 logger: logger, 100 metrics: metrics, 101 path: path, 102 pathExclude: pathExclude, 103 labels: labels, 104 discoveredLabels: discoveredLabels, 105 handler: api.AddLabelsMiddleware(labels).Wrap(handler), 106 positions: positions, 107 quit: make(chan struct{}), 108 done: make(chan struct{}), 109 tails: map[string]*tailer{}, 110 targetConfig: targetConfig, 111 fileEventWatcher: fileEventWatcher, 112 targetEventHandler: targetEventHandler, 113 encoding: encoding, 114 } 115 116 go t.run() 117 return t, nil 118 } 119 120 // Ready if at least one file is being tailed 121 func (t *FileTarget) Ready() bool { 122 return len(t.tails) > 0 123 } 124 125 // Stop the target. 126 func (t *FileTarget) Stop() { 127 close(t.quit) 128 <-t.done 129 t.handler.Stop() 130 } 131 132 // Type implements a Target 133 func (t *FileTarget) Type() target.TargetType { 134 return target.FileTargetType 135 } 136 137 // DiscoveredLabels implements a Target 138 func (t *FileTarget) DiscoveredLabels() model.LabelSet { 139 return t.discoveredLabels 140 } 141 142 // Labels implements a Target 143 func (t *FileTarget) Labels() model.LabelSet { 144 return t.labels 145 } 146 147 // Details implements a Target 148 func (t *FileTarget) Details() interface{} { 149 files := map[string]int64{} 150 for fileName := range t.tails { 151 files[fileName], _ = t.positions.Get(fileName) 152 } 153 return files 154 } 155 156 func (t *FileTarget) run() { 157 defer func() { 158 for _, v := range t.tails { 159 v.stop() 160 } 161 level.Info(t.logger).Log("msg", "filetarget: watcher closed, tailer stopped, positions saved", "path", t.path) 162 close(t.done) 163 }() 164 165 err := t.sync() 166 if err != nil { 167 level.Error(t.logger).Log("msg", "error running sync function", "error", err) 168 } 169 170 ticker := time.NewTicker(t.targetConfig.SyncPeriod) 171 defer ticker.Stop() 172 173 for { 174 select { 175 case event, ok := <-t.fileEventWatcher: 176 if !ok { 177 // fileEventWatcher has been closed 178 return 179 } 180 switch event.Op { 181 case fsnotify.Create: 182 t.startTailing([]string{event.Name}) 183 default: 184 // No-op we only care about Create events 185 } 186 case <-ticker.C: 187 err := t.sync() 188 if err != nil { 189 level.Error(t.logger).Log("msg", "error running sync function", "error", err) 190 } 191 case <-t.quit: 192 return 193 } 194 } 195 } 196 197 func (t *FileTarget) sync() error { 198 var matches, matchesExcluded []string 199 if fi, err := os.Stat(t.path); err == nil && !fi.IsDir() { 200 // if the path points to a file that exists, then it we can skip the Glob search 201 matches = []string{t.path} 202 } else { 203 // Gets current list of files to tail. 204 matches, err = doublestar.Glob(t.path) 205 if err != nil { 206 return errors.Wrap(err, "filetarget.sync.filepath.Glob") 207 } 208 } 209 210 if fi, err := os.Stat(t.pathExclude); err == nil && !fi.IsDir() { 211 matchesExcluded = []string{t.pathExclude} 212 } else { 213 matchesExcluded, err = doublestar.Glob(t.pathExclude) 214 if err != nil { 215 return errors.Wrap(err, "filetarget.sync.filepathexclude.Glob") 216 } 217 } 218 219 for i := 0; i < len(matchesExcluded); i++ { 220 for j := 0; j < len(matches); j++ { 221 if matchesExcluded[i] == matches[j] { 222 // exclude this specific match 223 matches = append(matches[:j], matches[j+1:]...) 224 } 225 } 226 } 227 228 if len(matches) == 0 { 229 level.Debug(t.logger).Log("msg", "no files matched requested path, nothing will be tailed", "path", t.path, "pathExclude", t.pathExclude) 230 } 231 232 // Gets absolute path for each pattern. 233 for i := 0; i < len(matches); i++ { 234 if !filepath.IsAbs(matches[i]) { 235 path, err := filepath.Abs(matches[i]) 236 if err != nil { 237 return errors.Wrap(err, "filetarget.sync.filepath.Abs") 238 } 239 matches[i] = path 240 } 241 } 242 243 // Record the size of all the files matched by the Glob pattern. 244 t.reportSize(matches) 245 246 // Get the current unique set of dirs to watch. 247 dirs := map[string]struct{}{} 248 for _, p := range matches { 249 dirs[filepath.Dir(p)] = struct{}{} 250 } 251 252 // Add any directories which are not already being watched. 253 toStartWatching := missing(t.watches, dirs) 254 t.startWatching(toStartWatching) 255 256 // Remove any directories which no longer need watching. 257 toStopWatching := missing(dirs, t.watches) 258 t.stopWatching(toStopWatching) 259 260 // fsnotify.Watcher doesn't allow us to see what is currently being watched so we have to track it ourselves. 261 t.watches = dirs 262 263 // Check if any running tailers have stopped because of errors and remove them from the running list 264 // (They will be restarted in startTailing) 265 t.pruneStoppedTailers() 266 267 // Start tailing all of the matched files if not already doing so. 268 t.startTailing(matches) 269 270 // Stop tailing any files which no longer exist 271 toStopTailing := toStopTailing(matches, t.tails) 272 t.stopTailingAndRemovePosition(toStopTailing) 273 274 return nil 275 } 276 277 func (t *FileTarget) startWatching(dirs map[string]struct{}) { 278 for dir := range dirs { 279 if _, ok := t.watches[dir]; ok { 280 continue 281 } 282 level.Info(t.logger).Log("msg", "watching new directory", "directory", dir) 283 t.targetEventHandler <- fileTargetEvent{ 284 path: dir, 285 eventType: fileTargetEventWatchStart, 286 } 287 } 288 } 289 290 func (t *FileTarget) stopWatching(dirs map[string]struct{}) { 291 for dir := range dirs { 292 if _, ok := t.watches[dir]; !ok { 293 continue 294 } 295 level.Info(t.logger).Log("msg", "removing directory from watcher", "directory", dir) 296 t.targetEventHandler <- fileTargetEvent{ 297 path: dir, 298 eventType: fileTargetEventWatchStop, 299 } 300 } 301 } 302 303 func (t *FileTarget) startTailing(ps []string) { 304 for _, p := range ps { 305 if _, ok := t.tails[p]; ok { 306 continue 307 } 308 309 fi, err := os.Stat(p) 310 if err != nil { 311 level.Error(t.logger).Log("msg", "failed to tail file, stat failed", "error", err, "filename", p) 312 t.metrics.totalBytes.DeleteLabelValues(p) 313 continue 314 } 315 316 if fi.IsDir() { 317 level.Info(t.logger).Log("msg", "failed to tail file", "error", "file is a directory", "filename", p) 318 t.metrics.totalBytes.DeleteLabelValues(p) 319 continue 320 } 321 322 level.Debug(t.logger).Log("msg", "tailing new file", "filename", p) 323 tailer, err := newTailer(t.metrics, t.logger, t.handler, t.positions, p, t.encoding) 324 if err != nil { 325 level.Error(t.logger).Log("msg", "failed to start tailer", "error", err, "filename", p) 326 continue 327 } 328 t.tails[p] = tailer 329 } 330 } 331 332 // stopTailingAndRemovePosition will stop the tailer and remove the positions entry. 333 // Call this when a file no longer exists and you want to remove all traces of it. 334 func (t *FileTarget) stopTailingAndRemovePosition(ps []string) { 335 for _, p := range ps { 336 if tailer, ok := t.tails[p]; ok { 337 tailer.stop() 338 t.positions.Remove(tailer.path) 339 delete(t.tails, p) 340 } 341 if h, ok := t.handler.(api.InstrumentedEntryHandler); ok { 342 h.UnregisterLatencyMetric(prometheus.Labels{client.LatencyLabel: p}) 343 } 344 } 345 } 346 347 // pruneStoppedTailers removes any tailers which have stopped running from 348 // the list of active tailers. This allows them to be restarted if there were errors. 349 func (t *FileTarget) pruneStoppedTailers() { 350 toRemove := make([]string, 0, len(t.tails)) 351 for k, t := range t.tails { 352 if !t.isRunning() { 353 toRemove = append(toRemove, k) 354 } 355 } 356 for _, tr := range toRemove { 357 delete(t.tails, tr) 358 } 359 } 360 361 func toStopTailing(nt []string, et map[string]*tailer) []string { 362 // Make a set of all existing tails 363 existingTails := make(map[string]struct{}, len(et)) 364 for file := range et { 365 existingTails[file] = struct{}{} 366 } 367 // Make a set of what we are about to start tailing 368 newTails := make(map[string]struct{}, len(nt)) 369 for _, p := range nt { 370 newTails[p] = struct{}{} 371 } 372 // Find the tails in our existing which are not in the new, these need to be stopped! 373 ts := missing(newTails, existingTails) 374 ta := make([]string, len(ts)) 375 i := 0 376 for t := range ts { 377 ta[i] = t 378 i++ 379 } 380 return ta 381 } 382 383 func (t *FileTarget) reportSize(ms []string) { 384 for _, m := range ms { 385 // Ask the tailer to update the size if a tailer exists, this keeps position and size metrics in sync 386 if tailer, ok := t.tails[m]; ok { 387 err := tailer.markPositionAndSize() 388 if err != nil { 389 level.Warn(t.logger).Log("msg", "failed to get file size from tailer, ", "file", m, "error", err) 390 return 391 } 392 } else { 393 // Must be a new file, just directly read the size of it 394 fi, err := os.Stat(m) 395 if err != nil { 396 // If the file was deleted between when the glob match and here, 397 // we just ignore recording a size for it, 398 // the tail code will also check if the file exists before creating a tailer. 399 return 400 } 401 t.metrics.totalBytes.WithLabelValues(m).Set(float64(fi.Size())) 402 } 403 404 } 405 } 406 407 // Returns the elements from set b which are missing from set a 408 func missing(as map[string]struct{}, bs map[string]struct{}) map[string]struct{} { 409 c := map[string]struct{}{} 410 for a := range bs { 411 if _, ok := as[a]; !ok { 412 c[a] = struct{}{} 413 } 414 } 415 return c 416 }