diff options
Diffstat (limited to '')
-rw-r--r-- | src/go/plugin/go.d/pkg/logs/reader.go | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/pkg/logs/reader.go b/src/go/plugin/go.d/pkg/logs/reader.go new file mode 100644 index 000000000..55f0ee18f --- /dev/null +++ b/src/go/plugin/go.d/pkg/logs/reader.go @@ -0,0 +1,193 @@ +// 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 "" +} |