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  }