// SPDX-License-Identifier: GPL-3.0-or-later package logs import ( "errors" "fmt" "io" "os" "path/filepath" "sort" "github.com/netdata/netdata/go/plugins/logger" ) const ( maxEOF = 60 ) var ( ErrNoMatchedFile = errors.New("no matched files") ) // Reader is a log rotate aware Reader // TODO: better reopen algorithm // TODO: handle truncate type Reader struct { file *os.File path string excludePath string eofCounter int continuousEOF int log *logger.Logger } // Open a file and seek to end of the file. // path: the shell file name pattern // excludePath: the shell file name pattern func Open(path string, excludePath string, log *logger.Logger) (*Reader, error) { var err error if path, err = filepath.Abs(path); err != nil { return nil, err } if _, err = filepath.Match(path, "/"); err != nil { return nil, fmt.Errorf("bad path syntax: %q", path) } if _, err = filepath.Match(excludePath, "/"); err != nil { return nil, fmt.Errorf("bad exclude_path syntax: %q", path) } r := &Reader{ path: path, excludePath: excludePath, log: log, } if err = r.open(); err != nil { return nil, err } return r, nil } // CurrentFilename get current opened file name func (r *Reader) CurrentFilename() string { return r.file.Name() } func (r *Reader) open() error { path := r.findFile() if path == "" { r.log.Debugf("couldn't find log file, used path: '%s', exclude_path: '%s'", r.path, r.excludePath) return ErrNoMatchedFile } r.log.Debug("open log file: ", path) file, err := os.Open(path) if err != nil { return err } stat, err := file.Stat() if err != nil { return err } if _, err = file.Seek(stat.Size(), io.SeekStart); err != nil { return err } r.file = file return nil } func (r *Reader) Read(p []byte) (n int, err error) { n, err = r.file.Read(p) if err != nil { switch { case err == io.EOF: err = r.handleEOFErr() case errors.Is(err, os.ErrInvalid): // r.file is nil after Close err = r.handleInvalidArgErr() } return } r.continuousEOF = 0 return } func (r *Reader) handleEOFErr() (err error) { err = io.EOF r.eofCounter++ r.continuousEOF++ if r.eofCounter < maxEOF || r.continuousEOF < 2 { return err } if err2 := r.reopen(); err2 != nil { err = err2 } return err } func (r *Reader) handleInvalidArgErr() (err error) { err = io.EOF if err2 := r.reopen(); err2 != nil { err = err2 } return err } func (r *Reader) Close() (err error) { if r == nil || r.file == nil { return } r.log.Debug("close log file: ", r.file.Name()) err = r.file.Close() r.file = nil r.eofCounter = 0 return } func (r *Reader) reopen() error { r.log.Debugf("reopen, look for: %s", r.path) _ = r.Close() return r.open() } func (r *Reader) findFile() string { return find(r.path, r.excludePath) } func find(path, exclude string) string { return finder{}.find(path, exclude) } // TODO: tests type finder struct{} func (f finder) find(path, exclude string) string { files, _ := filepath.Glob(path) if len(files) == 0 { return "" } files = f.filter(files, exclude) if len(files) == 0 { return "" } return f.findLastFile(files) } func (f finder) filter(files []string, exclude string) []string { if exclude == "" { return files } fs := make([]string, 0, len(files)) for _, file := range files { if ok, _ := filepath.Match(exclude, file); ok { continue } fs = append(fs, file) } return fs } // TODO: the logic is probably wrong func (f finder) findLastFile(files []string) string { sort.Strings(files) for i := len(files) - 1; i >= 0; i-- { stat, err := os.Stat(files[i]) if err != nil || !stat.Mode().IsRegular() { continue } return files[i] } return "" }