github.com/netdata/go.d.plugin@v0.58.1/pkg/logs/reader.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package logs
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  
    13  	"github.com/netdata/go.d.plugin/logger"
    14  )
    15  
    16  const (
    17  	maxEOF = 60
    18  )
    19  
    20  var (
    21  	ErrNoMatchedFile = errors.New("no matched files")
    22  )
    23  
    24  // Reader is a log rotate aware Reader
    25  // TODO: better reopen algorithm
    26  // TODO: handle truncate
    27  type Reader struct {
    28  	file          *os.File
    29  	path          string
    30  	excludePath   string
    31  	eofCounter    int
    32  	continuousEOF int
    33  	log           *logger.Logger
    34  }
    35  
    36  // Open a file and seek to end of the file.
    37  // path: the shell file name pattern
    38  // excludePath: the shell file name pattern
    39  func Open(path string, excludePath string, log *logger.Logger) (*Reader, error) {
    40  	var err error
    41  	if path, err = filepath.Abs(path); err != nil {
    42  		return nil, err
    43  	}
    44  	if _, err = filepath.Match(path, "/"); err != nil {
    45  		return nil, fmt.Errorf("bad path syntax: %q", path)
    46  	}
    47  	if _, err = filepath.Match(excludePath, "/"); err != nil {
    48  		return nil, fmt.Errorf("bad exclude_path syntax: %q", path)
    49  	}
    50  	r := &Reader{
    51  		path:        path,
    52  		excludePath: excludePath,
    53  		log:         log,
    54  	}
    55  
    56  	if err = r.open(); err != nil {
    57  		return nil, err
    58  	}
    59  	return r, nil
    60  }
    61  
    62  // CurrentFilename get current opened file name
    63  func (r *Reader) CurrentFilename() string {
    64  	return r.file.Name()
    65  }
    66  
    67  func (r *Reader) open() error {
    68  	path := r.findFile()
    69  	if path == "" {
    70  		r.log.Debugf("couldn't find log file, used path: '%s', exclude_path: '%s'", r.path, r.excludePath)
    71  		return ErrNoMatchedFile
    72  	}
    73  	r.log.Debug("open log file: ", path)
    74  	file, err := os.Open(path)
    75  	if err != nil {
    76  		return err
    77  	}
    78  	stat, err := file.Stat()
    79  	if err != nil {
    80  		return err
    81  	}
    82  	if _, err = file.Seek(stat.Size(), io.SeekStart); err != nil {
    83  		return err
    84  	}
    85  	r.file = file
    86  	return nil
    87  }
    88  
    89  func (r *Reader) Read(p []byte) (n int, err error) {
    90  	n, err = r.file.Read(p)
    91  	if err != nil {
    92  		switch err {
    93  		case io.EOF:
    94  			err = r.handleEOFErr()
    95  		case os.ErrInvalid: // r.file is nil after Close
    96  			err = r.handleInvalidArgErr()
    97  		}
    98  		return
    99  	}
   100  	r.continuousEOF = 0
   101  	return
   102  }
   103  
   104  func (r *Reader) handleEOFErr() (err error) {
   105  	err = io.EOF
   106  	r.eofCounter++
   107  	r.continuousEOF++
   108  	if r.eofCounter < maxEOF || r.continuousEOF < 2 {
   109  		return err
   110  	}
   111  	if err2 := r.reopen(); err2 != nil {
   112  		err = err2
   113  	}
   114  	return err
   115  }
   116  
   117  func (r *Reader) handleInvalidArgErr() (err error) {
   118  	err = io.EOF
   119  	if err2 := r.reopen(); err2 != nil {
   120  		err = err2
   121  	}
   122  	return err
   123  }
   124  
   125  func (r *Reader) Close() (err error) {
   126  	if r == nil || r.file == nil {
   127  		return
   128  	}
   129  	r.log.Debug("close log file: ", r.file.Name())
   130  	err = r.file.Close()
   131  	r.file = nil
   132  	r.eofCounter = 0
   133  	return
   134  }
   135  
   136  func (r *Reader) reopen() error {
   137  	r.log.Debugf("reopen, look for: %s", r.path)
   138  	_ = r.Close()
   139  	return r.open()
   140  }
   141  
   142  func (r *Reader) findFile() string {
   143  	return find(r.path, r.excludePath)
   144  }
   145  
   146  func find(path, exclude string) string {
   147  	return finder{}.find(path, exclude)
   148  }
   149  
   150  // TODO: tests
   151  type finder struct{}
   152  
   153  func (f finder) find(path, exclude string) string {
   154  	files, _ := filepath.Glob(path)
   155  	if len(files) == 0 {
   156  		return ""
   157  	}
   158  
   159  	files = f.filter(files, exclude)
   160  	if len(files) == 0 {
   161  		return ""
   162  	}
   163  
   164  	return f.findLastFile(files)
   165  }
   166  
   167  func (f finder) filter(files []string, exclude string) []string {
   168  	if exclude == "" {
   169  		return files
   170  	}
   171  
   172  	fs := make([]string, 0, len(files))
   173  	for _, file := range files {
   174  		if ok, _ := filepath.Match(exclude, file); ok {
   175  			continue
   176  		}
   177  		fs = append(fs, file)
   178  	}
   179  	return fs
   180  }
   181  
   182  // TODO: the logic is probably wrong
   183  func (f finder) findLastFile(files []string) string {
   184  	sort.Strings(files)
   185  	for i := len(files) - 1; i >= 0; i-- {
   186  		stat, err := os.Stat(files[i])
   187  		if err != nil || !stat.Mode().IsRegular() {
   188  			continue
   189  		}
   190  		return files[i]
   191  	}
   192  	return ""
   193  }