forked from hhkbp2/go-logging
-
Notifications
You must be signed in to change notification settings - Fork 0
/
handler_timed_rotating_file.go
336 lines (317 loc) · 9.63 KB
/
handler_timed_rotating_file.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package logging
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/hhkbp2/go-strftime"
)
const (
Day = 24 * time.Hour
Week = 7 * Day
)
var (
ErrorInvalidFormat = errors.New("invalid format")
)
// Handler for logging to a file, rotating the log file at certain timed
// intervals.
//
// if backupCount is > 0, when rollover is done, no more than backupCount
// files are kept - the oldest ones are deleted.
type TimedRotatingFileHandler struct {
*BaseRotatingHandler
when string
weekday int
interval time.Duration
rolloverTime time.Time
backupCount uint32
suffix string
extMatch string
utc bool
bufferFlushTime time.Duration
inputChanSize int
handleFunc HandleFunc
inputChan chan *LogRecord
group *sync.WaitGroup
}
// Note: weekday index starts from 0(Monday) to 6(Sunday) in Python.
// But in Golang weekday index starts from 0(Sunday) to 6(Saturday).
// Here we stick to semantics of the original Python logging interface.
func NewTimedRotatingFileHandler(
filepath string,
mode int,
bufferSize int,
bufferFlushTime time.Duration,
inputChanSize int,
when string,
interval uint32,
backupCount uint32,
utc bool) (*TimedRotatingFileHandler, error) {
var timeInterval time.Duration
var suffix, extMatch string
var weekday int
// Calculate the real rollover interval, which is just the number seconds
// between rollovers. Also set the filename suffix used when a rollover
// occurs. Current 'when' events supported:
// S - Seconds
// M - Minutes
// H - Hours
// D - Days
// midnight - roll over at midnight
// W{0-6} - roll over on a certain weekday; 0 - Monday
// Case of the 'when' specifier is not important; lower or upper case
// will work.
when = strings.ToUpper(when)
switch {
case when == "S":
timeInterval = time.Second
suffix = "%Y-%m-%d_%H-%M-%S"
extMatch = `^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$`
case when == "M":
timeInterval = time.Minute
suffix = "%Y-%m-%d_%H-%M"
extMatch = `^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$`
case when == "H":
timeInterval = time.Hour
suffix = "%Y-%m-%d_%H"
extMatch = `^\d{4}-\d{2}-\d{2}_\d{2}$`
case (when == "D") || (when == "MIDNIGHT"):
timeInterval = Day
suffix = "%Y-%m-%d"
extMatch = `^\d{4}-\d{2}-\d{2}$`
case strings.HasPrefix(when, "W"):
timeInterval = Week
if len(when) != 2 {
return nil, ErrorInvalidFormat
}
dayChar := when[1]
if (dayChar < '0') || (dayChar > '6') {
return nil, ErrorInvalidFormat
}
// cast Python style index value to Golang style index value
weekday = (int(dayChar-'0') + 1) % 7
suffix = "%Y-%m-%d"
extMatch = `^\d{4}-\d{2}-\d{2}$`
default:
return nil, ErrorInvalidFormat
}
timeInterval = time.Duration(int64(timeInterval) * int64(interval))
baseHandler, err := NewBaseRotatingHandler(filepath, mode, bufferSize)
if err != nil {
return nil, err
}
fileInfo, err := os.Stat(baseHandler.GetFilePath())
if err != nil {
baseHandler.Close()
return nil, err
}
object := &TimedRotatingFileHandler{
BaseRotatingHandler: baseHandler,
when: when,
weekday: weekday,
interval: timeInterval,
backupCount: backupCount,
suffix: suffix,
extMatch: extMatch,
utc: utc,
bufferFlushTime: bufferFlushTime,
inputChanSize: inputChanSize,
}
object.rolloverTime = object.computeRolloverTime(fileInfo.ModTime())
// register object to closer
Closer.RemoveHandler(object.BaseRotatingHandler)
Closer.AddHandler(object)
if inputChanSize > 0 {
object.handleFunc = object.handleChan
object.inputChan = make(chan *LogRecord, inputChanSize)
object.group = &sync.WaitGroup{}
object.group.Add(1)
go func() {
defer object.group.Done()
object.loop()
}()
} else {
object.handleFunc = object.handleCall
}
return object, nil
}
// Work out the rollover time based on the specified time.
func (self *TimedRotatingFileHandler) computeRolloverTime(
currentTime time.Time) time.Time {
result := currentTime.Add(self.interval)
// If we are rolling over at midnight or weekly, then the interval is
// already known. What we need to figure out is WHEN the next interval is.
// In other words, if you are rolling over at midnight, then
// your base interval is 1 day, but you want to start that one day clock
// at midnight, not now.
// So, we have to fudge the rolloverTime value in order trigger the first
// rollover at the right time. After that, the regular interval will
// take care of the rest.
// Note that this code doesn't care about leap seconds.
if (self.when == "MIDNIGHT") || strings.HasPrefix(self.when, "W") {
var t time.Time
if self.utc {
t = currentTime.UTC()
} else {
t = currentTime.Local()
}
dayStartTime := time.Date(
t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
result = currentTime.Add(Day - t.Sub(dayStartTime))
// If we are rolling over on a certain day, add in the number of days
// until the next rollover, but offset by 1 since we just calculated
// the time until the next day starts. There are three cases:
// Case 1) The day to rollover is today; in this case, do nothing
// Case 2) The day to rollover is further in the interval (i.e.,
// today is day 3 (Wednesday) and rollover is on day 6
// (Saturday). Days to next rollover is simply 6 - 3, or 3)
// Case 3) The day to rollover is behind us in the interval (i.e.,
// today is day 5 (Friday) and rollover is on day 4 (Thursday).
// Days to rollover is 6 - 5 + 4 + 1, or 6.) In this case,
// it's the number of days left in the current week (1) plus
// the number of days in the next week until the
// rollover day (5).
// THe calculations described in 2) and 3) above need to
// have a day added. This is because the above time calculation
// takes us to midnight on this day, i.e., the start of the next day.
if strings.HasPrefix(self.when, "W") {
weekday := int(t.Weekday())
if weekday != self.weekday {
var daysToWait int
if weekday < self.weekday {
daysToWait = self.weekday - weekday
} else {
daysToWait = 6 - weekday + self.weekday + 1
}
result = result.Add(time.Duration(int64(daysToWait) * int64(Day)))
// NOTE: we skip the daylight savings time issues here
// because time library in Golang doesn't support it.
}
}
}
return result
}
// Determine if rollover should occur.
func (self *TimedRotatingFileHandler) ShouldRollover(
record *LogRecord) (bool, string) {
overTime := time.Now().After(self.rolloverTime)
return overTime, self.Format(record)
}
// Determine the files to delete when rolling over.
func (self *TimedRotatingFileHandler) getFilesToDelete() ([]string, error) {
dirName, baseName := filepath.Split(self.GetFilePath())
fileInfos, err := ioutil.ReadDir(dirName)
if err != nil {
return nil, err
}
prefix := baseName + "."
pattern, err := regexp.Compile(self.extMatch)
if err != nil {
return nil, err
}
var fileNames []string
for _, info := range fileInfos {
fileName := info.Name()
if strings.HasPrefix(fileName, prefix) {
suffix := fileName[len(prefix):]
if pattern.MatchString(suffix) {
fileNames = append(fileNames, fileName)
}
}
}
// no need to sort fileNames since ioutil.ReadDir() returns sorted list.
var result []string
if uint32(len(fileNames)) < self.backupCount {
return result, nil
}
result = fileNames[:uint32(len(fileNames))-self.backupCount]
for i := 0; i < len(result); i++ {
result[i] = filepath.Join(dirName, result[i])
}
return result, nil
}
// Do a rollover; in this case, a date/time stamp is appended to the filename
// when the rollover happens. However, you want the file to be named for
// the start of the interval, not the current time. If there is a backup
// count, then we have to get a list of matching filenames, sort them and
// remove the one with the oldest suffix.
func (self *TimedRotatingFileHandler) DoRollover() (err error) {
self.FileHandler.Close()
defer func() {
if e := self.FileHandler.Open(); e != nil {
if e != nil {
err = e
}
}
}()
currentTime := time.Now()
t := self.rolloverTime.Add(time.Duration(-int64(self.interval)))
if self.utc {
t = t.UTC()
} else {
t = t.Local()
}
baseFilename := self.GetFilePath()
dfn := baseFilename + "." + strftime.Format(self.suffix, t)
if FileExists(dfn) {
if err := os.Remove(dfn); err != nil {
return err
}
}
if err := os.Rename(baseFilename, dfn); err != nil {
return err
}
if self.backupCount > 0 {
files, err := self.getFilesToDelete()
if err != nil {
return err
}
for _, f := range files {
if err := os.Remove(f); err != nil {
return err
}
}
}
self.rolloverTime = self.computeRolloverTime(currentTime)
return nil
}
// Emit a record.
func (self *TimedRotatingFileHandler) Emit(record *LogRecord) error {
return self.RolloverEmit(self, record)
}
func (self *TimedRotatingFileHandler) handleCall(record *LogRecord) int {
return self.Handle2(self, record)
}
func (self *TimedRotatingFileHandler) handleChan(record *LogRecord) int {
self.inputChan <- record
return 0
}
func (self *TimedRotatingFileHandler) loop() {
ticker := time.NewTicker(self.bufferFlushTime)
for {
select {
case r := <-self.inputChan:
if r == nil {
return
}
self.Handle2(self, r)
case <-ticker.C:
self.Flush()
}
}
}
func (self *TimedRotatingFileHandler) Handle(record *LogRecord) int {
return self.handleFunc(record)
}
func (self *TimedRotatingFileHandler) Close() {
if self.inputChanSize > 0 {
// send a nil record as "stop signal" to exit loop.
self.inputChan <- nil
self.group.Wait()
}
self.BaseRotatingHandler.Close()
}