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 }