| 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/netdata/go/plugins/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", excludePath) |
| 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 | if r.file == nil { |
| 65 | return "" |
| 66 | } |
| 67 | return r.file.Name() |
| 68 | } |
| 69 | |
| 70 | func (r *Reader) open() error { |
| 71 | path := r.findFile() |
| 72 | if path == "" { |
| 73 | r.log.Debugf("couldn't find log file, used path: '%s', exclude_path: '%s'", r.path, r.excludePath) |
| 74 | return ErrNoMatchedFile |
| 75 | } |
| 76 | r.log.Debug("open log file: ", path) |
| 77 | file, err := os.Open(path) |
| 78 | if err != nil { |
| 79 | return err |
| 80 | } |
| 81 | stat, err := file.Stat() |
| 82 | if err != nil { |
| 83 | _ = file.Close() |
| 84 | return err |
| 85 | } |
| 86 | if _, err = file.Seek(stat.Size(), io.SeekStart); err != nil { |
| 87 | _ = file.Close() |
| 88 | return err |
| 89 | } |
| 90 | r.file = file |
| 91 | return nil |
| 92 | } |
| 93 | |
| 94 | func (r *Reader) Read(p []byte) (n int, err error) { |
| 95 | n, err = r.file.Read(p) |
| 96 | if err != nil { |
| 97 | switch { |
| 98 | case err == io.EOF: |
| 99 | err = r.handleEOFErr() |
| 100 | case errors.Is(err, os.ErrInvalid): // r.file is nil after Close |
| 101 | err = r.handleInvalidArgErr() |
| 102 | } |
| 103 | return |
| 104 | } |
| 105 | r.continuousEOF = 0 |
| 106 | return |
| 107 | } |
| 108 | |
| 109 | func (r *Reader) handleEOFErr() (err error) { |
| 110 | err = io.EOF |
| 111 | r.eofCounter++ |
| 112 | r.continuousEOF++ |
| 113 | if r.eofCounter < maxEOF || r.continuousEOF < 2 { |
| 114 | return err |
| 115 | } |
| 116 | if err2 := r.reopen(); err2 != nil { |
| 117 | err = err2 |
| 118 | } |
| 119 | return err |
| 120 | } |
| 121 | |
| 122 | func (r *Reader) handleInvalidArgErr() (err error) { |
| 123 | err = io.EOF |
| 124 | if err2 := r.reopen(); err2 != nil { |
| 125 | err = err2 |
| 126 | } |
| 127 | return err |
| 128 | } |
| 129 | |
| 130 | func (r *Reader) Close() (err error) { |
| 131 | if r == nil || r.file == nil { |
| 132 | return |
| 133 | } |
| 134 | r.log.Debug("close log file: ", r.file.Name()) |
| 135 | err = r.file.Close() |
| 136 | r.file = nil |
| 137 | r.continuousEOF = 0 |
| 138 | r.eofCounter = 0 |
| 139 | return |
| 140 | } |
| 141 | |
| 142 | func (r *Reader) reopen() error { |
| 143 | r.log.Debugf("reopen, look for: %s", r.path) |
| 144 | _ = r.Close() |
| 145 | return r.open() |
| 146 | } |
| 147 | |
| 148 | func (r *Reader) findFile() string { |
| 149 | return find(r.path, r.excludePath) |
| 150 | } |
| 151 | |
| 152 | func find(path, exclude string) string { |
| 153 | return finder{}.find(path, exclude) |
| 154 | } |
| 155 | |
| 156 | // TODO: tests |
| 157 | type finder struct{} |
| 158 | |
| 159 | func (f finder) find(path, exclude string) string { |
| 160 | files, _ := filepath.Glob(path) |
| 161 | if len(files) == 0 { |
| 162 | return "" |
| 163 | } |
| 164 | |
| 165 | files = f.filter(files, exclude) |
| 166 | if len(files) == 0 { |
| 167 | return "" |
| 168 | } |
| 169 | |
| 170 | return f.findLastFile(files) |
| 171 | } |
| 172 | |
| 173 | func (f finder) filter(files []string, exclude string) []string { |
| 174 | if exclude == "" { |
| 175 | return files |
| 176 | } |
| 177 | |
| 178 | fs := make([]string, 0, len(files)) |
| 179 | for _, file := range files { |
| 180 | if ok, _ := filepath.Match(exclude, file); ok { |
| 181 | continue |
| 182 | } |
| 183 | fs = append(fs, file) |
| 184 | } |
| 185 | return fs |
| 186 | } |
| 187 | |
| 188 | // TODO: the logic is probably wrong |
| 189 | func (f finder) findLastFile(files []string) string { |
| 190 | sort.Strings(files) |
| 191 | for i := len(files) - 1; i >= 0; i-- { |
| 192 | stat, err := os.Stat(files[i]) |
| 193 | if err != nil || !stat.Mode().IsRegular() { |
| 194 | continue |
| 195 | } |
| 196 | return files[i] |
| 197 | } |
| 198 | return "" |
| 199 | } |