github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/clients/pkg/promtail/positions/positions.go (about) 1 package positions 2 3 import ( 4 "flag" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/go-kit/log" 15 "github.com/go-kit/log/level" 16 yaml "gopkg.in/yaml.v2" 17 ) 18 19 const ( 20 positionFileMode = 0600 21 cursorKeyPrefix = "cursor-" 22 journalKeyPrefix = "journal-" 23 ) 24 25 // Config describes where to get position information from. 26 type Config struct { 27 SyncPeriod time.Duration `yaml:"sync_period"` 28 PositionsFile string `yaml:"filename"` 29 IgnoreInvalidYaml bool `yaml:"ignore_invalid_yaml"` 30 ReadOnly bool `yaml:"-"` 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+"positions.sync-period", 10*time.Second, "Period with this to sync the position file.") 37 f.StringVar(&cfg.PositionsFile, prefix+"positions.file", "/var/log/positions.yaml", "Location to read/write positions from.") 38 f.BoolVar(&cfg.IgnoreInvalidYaml, prefix+"positions.ignore-invalid-yaml", false, "whether to ignore & later overwrite positions files that are corrupted") 39 } 40 41 // RegisterFlags register flags. 42 func (cfg *Config) RegisterFlags(flags *flag.FlagSet) { 43 cfg.RegisterFlagsWithPrefix("", flags) 44 } 45 46 // Positions tracks how far through each file we've read. 47 type positions struct { 48 logger log.Logger 49 cfg Config 50 mtx sync.Mutex 51 positions map[string]string 52 quit chan struct{} 53 done chan struct{} 54 } 55 56 // File format for the positions data. 57 type File struct { 58 Positions map[string]string `yaml:"positions"` 59 } 60 61 type Positions interface { 62 // GetString returns how far we've through a file as a string. 63 // JournalTarget writes a journal cursor to the positions file, while 64 // FileTarget writes an integer offset. Use Get to read the integer 65 // offset. 66 GetString(path string) string 67 // Get returns how far we've read through a file. Returns an error 68 // if the value stored for the file is not an integer. 69 Get(path string) (int64, error) 70 // PutString records (asynchronously) how far we've read through a file. 71 // Unlike Put, it records a string offset and is only useful for 72 // JournalTargets which doesn't have integer offsets. 73 PutString(path string, pos string) 74 // Put records (asynchronously) how far we've read through a file. 75 Put(path string, pos int64) 76 // Remove removes the position tracking for a filepath 77 Remove(path string) 78 // SyncPeriod returns how often the positions file gets resynced 79 SyncPeriod() time.Duration 80 // Stop the Position tracker. 81 Stop() 82 } 83 84 // New makes a new Positions. 85 func New(logger log.Logger, cfg Config) (Positions, error) { 86 positionData, err := readPositionsFile(cfg, logger) 87 if err != nil { 88 return nil, err 89 } 90 91 p := &positions{ 92 logger: logger, 93 cfg: cfg, 94 positions: positionData, 95 quit: make(chan struct{}), 96 done: make(chan struct{}), 97 } 98 99 go p.run() 100 return p, nil 101 } 102 103 func (p *positions) Stop() { 104 close(p.quit) 105 <-p.done 106 } 107 108 func (p *positions) PutString(path string, pos string) { 109 p.mtx.Lock() 110 defer p.mtx.Unlock() 111 p.positions[path] = pos 112 } 113 114 func (p *positions) Put(path string, pos int64) { 115 p.PutString(path, strconv.FormatInt(pos, 10)) 116 } 117 118 func (p *positions) GetString(path string) string { 119 p.mtx.Lock() 120 defer p.mtx.Unlock() 121 return p.positions[path] 122 } 123 124 func (p *positions) Get(path string) (int64, error) { 125 p.mtx.Lock() 126 defer p.mtx.Unlock() 127 pos, ok := p.positions[path] 128 if !ok { 129 return 0, nil 130 } 131 return strconv.ParseInt(pos, 10, 64) 132 } 133 134 func (p *positions) Remove(path string) { 135 p.mtx.Lock() 136 defer p.mtx.Unlock() 137 p.remove(path) 138 } 139 140 func (p *positions) remove(path string) { 141 delete(p.positions, path) 142 } 143 144 func (p *positions) SyncPeriod() time.Duration { 145 return p.cfg.SyncPeriod 146 } 147 148 func (p *positions) run() { 149 defer func() { 150 p.save() 151 level.Debug(p.logger).Log("msg", "positions saved") 152 close(p.done) 153 }() 154 155 ticker := time.NewTicker(p.cfg.SyncPeriod) 156 for { 157 select { 158 case <-p.quit: 159 return 160 case <-ticker.C: 161 p.save() 162 p.cleanup() 163 } 164 } 165 } 166 167 func (p *positions) save() { 168 if p.cfg.ReadOnly { 169 return 170 } 171 p.mtx.Lock() 172 positions := make(map[string]string, len(p.positions)) 173 for k, v := range p.positions { 174 positions[k] = v 175 } 176 p.mtx.Unlock() 177 178 if err := writePositionFile(p.cfg.PositionsFile, positions); err != nil { 179 level.Error(p.logger).Log("msg", "error writing positions file", "error", err) 180 } 181 } 182 183 // CursorKey returns a key that can be saved as a cursor that is never deleted. 184 func CursorKey(key string) string { 185 return fmt.Sprintf("%s%s", cursorKeyPrefix, key) 186 } 187 188 func (p *positions) cleanup() { 189 p.mtx.Lock() 190 defer p.mtx.Unlock() 191 toRemove := []string{} 192 for k := range p.positions { 193 // If the position file is prefixed with cursor, it's a 194 // cursor and not a file on disk. 195 // We still have to support journal files, so we keep the previous check to avoid breaking change. 196 if strings.HasPrefix(k, cursorKeyPrefix) || strings.HasPrefix(k, journalKeyPrefix) { 197 continue 198 } 199 200 if _, err := os.Stat(k); err != nil { 201 if os.IsNotExist(err) { 202 // File no longer exists. 203 toRemove = append(toRemove, k) 204 } else { 205 // Can't determine if file exists or not, some other error. 206 level.Warn(p.logger).Log("msg", "could not determine if log file "+ 207 "still exists while cleaning positions file", "error", err) 208 } 209 } 210 } 211 for _, tr := range toRemove { 212 p.remove(tr) 213 } 214 } 215 216 func readPositionsFile(cfg Config, logger log.Logger) (map[string]string, error) { 217 cleanfn := filepath.Clean(cfg.PositionsFile) 218 buf, err := ioutil.ReadFile(cleanfn) 219 if err != nil { 220 if os.IsNotExist(err) { 221 return map[string]string{}, nil 222 } 223 return nil, err 224 } 225 226 var p File 227 err = yaml.UnmarshalStrict(buf, &p) 228 if err != nil { 229 // return empty if cfg option enabled 230 if cfg.IgnoreInvalidYaml { 231 level.Debug(logger).Log("msg", "ignoring invalid positions file", "file", cleanfn, "error", err) 232 return map[string]string{}, nil 233 } 234 235 return nil, fmt.Errorf("invalid yaml positions file [%s]: %v", cleanfn, err) 236 } 237 238 // p.Positions will be nil if the file exists but is empty 239 if p.Positions == nil { 240 p.Positions = map[string]string{} 241 } 242 243 return p.Positions, nil 244 }