-
Notifications
You must be signed in to change notification settings - Fork 1
/
rotatewriter.go
294 lines (282 loc) · 7.36 KB
/
rotatewriter.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
// Package rotatewriter contains additional tool for logging packages - RotateWriter Writer which implement normal fast smooth rotation
package rotatewriter
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
)
// this file contains realizarion of Writer for logs which contains rotate capability
// RotateWriter is Writer with Rotate function to make correctly rotation of
type RotateWriter struct {
Filename string
NumFiles int
dirpath string
file *os.File
writeMutex sync.Mutex
rotateMutex sync.Mutex
statusMap sync.Map
IsBuffered bool
FlushTimeout time.Duration
bufferedOut *bufio.Writer
BufferSize int
lastWriteTime time.Time
ticker *time.Ticker
}
// NewRotateWriter creates new instance make some checks there
// fileName: filename, must contain existing directory file
// numfiles: 0 if no rotation at all - just reopen file on rotation. e.g. you would like use logrotate
// numfiles: >0 if rotation enabled
func NewRotateWriter(fileName string, numfiles int) (rw *RotateWriter, err error) {
rw = &RotateWriter{Filename: fileName, NumFiles: numfiles, file: nil}
err = rw.initDirPath()
if nil != err {
return nil, err
}
err = rw.openWriteFile()
if nil != err {
return nil, err
}
if 0 > numfiles {
return nil, fmt.Errorf("numfiles must be 0 or more")
}
return rw, nil
}
// NewRotateBufferedWriter creates new buffered instance make some checks there
// fileName: filename, must contain existing directory file
// numfiles: 0 if no rotation at all - just reopen file on rotation. e.g. you would like use logrotate
// numfiles: >0 if rotation enabled
// flush timeout - flush after timeout when there are no writes
// buffer size to work with
func NewRotateBufferedWriter(fileName string, numfiles int, flushTimeout time.Duration, bufferSize int) (rw *RotateWriter, err error) {
rw = &RotateWriter{Filename: fileName, NumFiles: numfiles, file: nil, IsBuffered: true, FlushTimeout: flushTimeout, BufferSize: bufferSize}
if flushTimeout == 0 {
return nil, fmt.Errorf("flushTimeout must be not nil")
}
if bufferSize == 0 {
return nil, fmt.Errorf("bufferSize must be non zero")
}
err = rw.initDirPath()
if nil != err {
return nil, err
}
err = rw.openWriteFile()
if nil != err {
return nil, err
}
if 0 > numfiles {
return nil, fmt.Errorf("numfiles must be 0 or more")
}
rw.lastWriteTime = time.Now()
rw.ticker = time.NewTicker(rw.FlushTimeout)
// start flush ticker
go func() {
for t := range rw.ticker.C {
func() {
rw.writeMutex.Lock()
defer rw.writeMutex.Unlock()
duration := t.Sub(rw.lastWriteTime)
if duration > rw.FlushTimeout && rw.bufferedOut != nil {
// flush that shit
rw.bufferedOut.Flush()
}
}()
}
}()
return rw, nil
}
// initDirPath gets dir path from filename and init them
func (rw *RotateWriter) initDirPath() error {
if rw.Filename == "" {
return fmt.Errorf("Wrong log path")
}
rw.dirpath = filepath.Dir(rw.Filename)
fileinfo, err := os.Stat(rw.dirpath)
if err != nil {
return err
}
if !fileinfo.IsDir() {
return fmt.Errorf("Path to log file %s is not directory", rw.dirpath)
}
return nil
}
// openWriteFile warning - is not safe - use Lock unlock while work
func (rw *RotateWriter) openWriteFile() error {
file, err := rw.openWriteFileInt()
if err != nil {
rw.file = nil
return err
}
rw.file = file
if rw.IsBuffered {
rw.bufferedOut = bufio.NewWriterSize(rw.file, rw.BufferSize)
rw.lastWriteTime = time.Now()
}
return nil
}
func (rw *RotateWriter) openWriteFileInt() (file *os.File, err error) {
fileinfo, err := os.Stat(rw.Filename)
newFile := false
if err != nil {
if os.IsNotExist(err) {
newFile = true
err = nil
} else {
return nil, err
}
}
if fileinfo == nil {
newFile = true
} else {
if fileinfo.IsDir() {
return nil, fmt.Errorf("File %s is a directory", rw.Filename)
}
}
if newFile {
file, err = os.OpenFile(rw.Filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
} else {
file, err = os.OpenFile(rw.Filename, os.O_APPEND|os.O_WRONLY, 0644)
}
return file, err
}
// CloseWriteFile use to close writer if you need
func (rw *RotateWriter) CloseWriteFile() error {
rw.writeMutex.Lock()
defer rw.writeMutex.Unlock()
if rw.file == nil {
return nil
}
if rw.IsBuffered {
rw.bufferedOut.Flush()
rw.bufferedOut = nil
}
err := rw.file.Close()
rw.file = nil
return err
}
// Write implements io.Writer
func (rw *RotateWriter) Write(p []byte) (n int, err error) {
rw.writeMutex.Lock()
defer rw.writeMutex.Unlock()
if rw.file == nil {
return 0, fmt.Errorf("Error: no file was opened for work with")
}
if rw.IsBuffered {
n, err = rw.bufferedOut.Write(p)
rw.lastWriteTime = time.Now()
} else {
n, err = rw.file.Write(p)
}
return n, err
}
// RotationInProgress detects rotation is running now
func (rw *RotateWriter) RotationInProgress() bool {
_, ok := rw.statusMap.Load("rotation")
return ok
}
// Rotate rotates file
func (rw *RotateWriter) Rotate(ready func()) error {
if _, ok := rw.statusMap.Load("rotation"); ok {
// rotation in progress - just prevent all fuckups
return nil
}
rw.rotateMutex.Lock()
defer rw.rotateMutex.Unlock()
defer func() {
if nil == ready {
return
}
ready()
}()
rw.statusMap.Store("rotation", true)
defer rw.statusMap.Delete("rotation")
files, err := ioutil.ReadDir(rw.dirpath)
if err != nil {
return err
}
_, fileName := filepath.Split(rw.Filename)
sl := make([]int, 0, rw.NumFiles)
if rw.NumFiles > 0 {
filesfor1:
for _, fi := range files {
if fi.IsDir() {
return fmt.Errorf("Rotation problem: File %s is directory", fi.Name())
}
ext := filepath.Ext(fi.Name())
if (fileName + ext) == fi.Name() {
if ext == "" {
continue filesfor1
}
ext = strings.Trim(ext, ".")
num1, err := strconv.ParseInt(ext, 10, 64)
num := int(num1)
if (err != nil) && !os.IsNotExist(err) {
continue filesfor1
}
if rw.NumFiles < num+1 { // unlink that shit
err = os.Remove(path.Join(rw.dirpath, fi.Name()))
if (err != nil) && !os.IsNotExist(err) {
return err
}
continue filesfor1
}
sl = append(sl, num)
}
}
sort.Slice(sl, func(i, j int) bool {
return sl[i] > sl[j]
})
for _, num := range sl {
err = os.Rename(
path.Join(rw.dirpath, fileName+"."+strconv.FormatInt(int64(num), 10)),
path.Join(rw.dirpath, fileName+"."+strconv.FormatInt(int64(num+1), 10)),
)
if err != nil {
return err
}
}
// may be we need errors here?
os.Rename(rw.Filename, rw.Filename+".1")
}
renewFile := true
if rw.NumFiles == 0 {
// here may be one fail: file was not renamed
_, err := os.Stat(rw.Filename)
renewFile = os.IsNotExist(err)
}
// right way first open file - not to make program wait while Write()
if renewFile { // if file was not deleted we really do not need reopen
oldfile := rw.file
oldBuff := rw.bufferedOut
newfile, err := rw.openWriteFileInt()
var newBuff *bufio.Writer
newBuff = nil
if rw.IsBuffered {
newBuff = bufio.NewWriterSize(newfile, rw.BufferSize)
}
if err != nil {
return err
}
func() { // just isolate rw.writeMutex work here
rw.writeMutex.Lock()
defer rw.writeMutex.Unlock()
// now file is opened. Just make save switch of them
rw.file = newfile
if rw.IsBuffered {
rw.bufferedOut = newBuff
}
}()
if rw.IsBuffered {
oldBuff.Flush()
}
oldfile.Close()
}
return nil
}