master
go 199 lines 3.76 KB
Raw
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 }