1// Copyright 2014 beego Author. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package logs
16
17import (
18	"bytes"
19	"encoding/json"
20	"errors"
21	"fmt"
22	"io"
23	"os"
24	"path"
25	"path/filepath"
26	"strconv"
27	"strings"
28	"sync"
29	"time"
30)
31
32// fileLogWriter implements LoggerInterface.
33// It writes messages by lines limit, file size limit, or time frequency.
34type fileLogWriter struct {
35	sync.RWMutex // write log order by order and  atomic incr maxLinesCurLines and maxSizeCurSize
36	// The opened file
37	Filename   string `json:"filename"`
38	fileWriter *os.File
39
40	// Rotate at line
41	MaxLines         int `json:"maxlines"`
42	maxLinesCurLines int
43
44	MaxFiles         int `json:"maxfiles"`
45	MaxFilesCurFiles int
46
47	// Rotate at size
48	MaxSize        int `json:"maxsize"`
49	maxSizeCurSize int
50
51	// Rotate daily
52	Daily         bool  `json:"daily"`
53	MaxDays       int64 `json:"maxdays"`
54	dailyOpenDate int
55	dailyOpenTime time.Time
56
57	// Rotate hourly
58	Hourly         bool  `json:"hourly"`
59	MaxHours       int64 `json:"maxhours"`
60	hourlyOpenDate int
61	hourlyOpenTime time.Time
62
63	Rotate bool `json:"rotate"`
64
65	Level int `json:"level"`
66
67	Perm string `json:"perm"`
68
69	RotatePerm string `json:"rotateperm"`
70
71	fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix
72}
73
74// newFileWriter create a FileLogWriter returning as LoggerInterface.
75func newFileWriter() Logger {
76	w := &fileLogWriter{
77		Daily:      true,
78		MaxDays:    7,
79		Hourly:     false,
80		MaxHours:   168,
81		Rotate:     true,
82		RotatePerm: "0440",
83		Level:      LevelTrace,
84		Perm:       "0660",
85		MaxLines:   10000000,
86		MaxFiles:   999,
87		MaxSize:    1 << 28,
88	}
89	return w
90}
91
92// Init file logger with json config.
93// jsonConfig like:
94//  {
95//  "filename":"logs/beego.log",
96//  "maxLines":10000,
97//  "maxsize":1024,
98//  "daily":true,
99//  "maxDays":15,
100//  "rotate":true,
101//      "perm":"0600"
102//  }
103func (w *fileLogWriter) Init(jsonConfig string) error {
104	err := json.Unmarshal([]byte(jsonConfig), w)
105	if err != nil {
106		return err
107	}
108	if len(w.Filename) == 0 {
109		return errors.New("jsonconfig must have filename")
110	}
111	w.suffix = filepath.Ext(w.Filename)
112	w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix)
113	if w.suffix == "" {
114		w.suffix = ".log"
115	}
116	err = w.startLogger()
117	return err
118}
119
120// start file logger. create log file and set to locker-inside file writer.
121func (w *fileLogWriter) startLogger() error {
122	file, err := w.createLogFile()
123	if err != nil {
124		return err
125	}
126	if w.fileWriter != nil {
127		w.fileWriter.Close()
128	}
129	w.fileWriter = file
130	return w.initFd()
131}
132
133func (w *fileLogWriter) needRotateDaily(size int, day int) bool {
134	return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) ||
135		(w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) ||
136		(w.Daily && day != w.dailyOpenDate)
137}
138
139func (w *fileLogWriter) needRotateHourly(size int, hour int) bool {
140	return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) ||
141		(w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) ||
142		(w.Hourly && hour != w.hourlyOpenDate)
143
144}
145
146// WriteMsg write logger message into file.
147func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error {
148	if level > w.Level {
149		return nil
150	}
151	hd, d, h := formatTimeHeader(when)
152	msg = string(hd) + msg + "\n"
153	if w.Rotate {
154		w.RLock()
155		if w.needRotateHourly(len(msg), h) {
156			w.RUnlock()
157			w.Lock()
158			if w.needRotateHourly(len(msg), h) {
159				if err := w.doRotate(when); err != nil {
160					fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
161				}
162			}
163			w.Unlock()
164		} else if w.needRotateDaily(len(msg), d) {
165			w.RUnlock()
166			w.Lock()
167			if w.needRotateDaily(len(msg), d) {
168				if err := w.doRotate(when); err != nil {
169					fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
170				}
171			}
172			w.Unlock()
173		} else {
174			w.RUnlock()
175		}
176	}
177
178	w.Lock()
179	_, err := w.fileWriter.Write([]byte(msg))
180	if err == nil {
181		w.maxLinesCurLines++
182		w.maxSizeCurSize += len(msg)
183	}
184	w.Unlock()
185	return err
186}
187
188func (w *fileLogWriter) createLogFile() (*os.File, error) {
189	// Open the log file
190	perm, err := strconv.ParseInt(w.Perm, 8, 64)
191	if err != nil {
192		return nil, err
193	}
194
195	filepath := path.Dir(w.Filename)
196	os.MkdirAll(filepath, os.FileMode(perm))
197
198	fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
199	if err == nil {
200		// Make sure file perm is user set perm cause of `os.OpenFile` will obey umask
201		os.Chmod(w.Filename, os.FileMode(perm))
202	}
203	return fd, err
204}
205
206func (w *fileLogWriter) initFd() error {
207	fd := w.fileWriter
208	fInfo, err := fd.Stat()
209	if err != nil {
210		return fmt.Errorf("get stat err: %s", err)
211	}
212	w.maxSizeCurSize = int(fInfo.Size())
213	w.dailyOpenTime = time.Now()
214	w.dailyOpenDate = w.dailyOpenTime.Day()
215	w.hourlyOpenTime = time.Now()
216	w.hourlyOpenDate = w.hourlyOpenTime.Hour()
217	w.maxLinesCurLines = 0
218	if w.Hourly {
219		go w.hourlyRotate(w.hourlyOpenTime)
220	} else if w.Daily {
221		go w.dailyRotate(w.dailyOpenTime)
222	}
223	if fInfo.Size() > 0 && w.MaxLines > 0 {
224		count, err := w.lines()
225		if err != nil {
226			return err
227		}
228		w.maxLinesCurLines = count
229	}
230	return nil
231}
232
233func (w *fileLogWriter) dailyRotate(openTime time.Time) {
234	y, m, d := openTime.Add(24 * time.Hour).Date()
235	nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location())
236	tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100))
237	<-tm.C
238	w.Lock()
239	if w.needRotateDaily(0, time.Now().Day()) {
240		if err := w.doRotate(time.Now()); err != nil {
241			fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
242		}
243	}
244	w.Unlock()
245}
246
247func (w *fileLogWriter) hourlyRotate(openTime time.Time) {
248	y, m, d := openTime.Add(1 * time.Hour).Date()
249	h, _, _ := openTime.Add(1 * time.Hour).Clock()
250	nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location())
251	tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100))
252	<-tm.C
253	w.Lock()
254	if w.needRotateHourly(0, time.Now().Hour()) {
255		if err := w.doRotate(time.Now()); err != nil {
256			fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
257		}
258	}
259	w.Unlock()
260}
261
262func (w *fileLogWriter) lines() (int, error) {
263	fd, err := os.Open(w.Filename)
264	if err != nil {
265		return 0, err
266	}
267	defer fd.Close()
268
269	buf := make([]byte, 32768) // 32k
270	count := 0
271	lineSep := []byte{'\n'}
272
273	for {
274		c, err := fd.Read(buf)
275		if err != nil && err != io.EOF {
276			return count, err
277		}
278
279		count += bytes.Count(buf[:c], lineSep)
280
281		if err == io.EOF {
282			break
283		}
284	}
285
286	return count, nil
287}
288
289// DoRotate means it need to write file in new file.
290// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size)
291func (w *fileLogWriter) doRotate(logTime time.Time) error {
292	// file exists
293	// Find the next available number
294	num := w.MaxFilesCurFiles + 1
295	fName := ""
296	format := ""
297	var openTime time.Time
298	rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64)
299	if err != nil {
300		return err
301	}
302
303	_, err = os.Lstat(w.Filename)
304	if err != nil {
305		//even if the file is not exist or other ,we should RESTART the logger
306		goto RESTART_LOGGER
307	}
308
309	if w.Hourly {
310		format = "2006010215"
311		openTime = w.hourlyOpenTime
312	} else if w.Daily {
313		format = "2006-01-02"
314		openTime = w.dailyOpenTime
315	}
316
317	// only when one of them be setted, then the file would be splited
318	if w.MaxLines > 0 || w.MaxSize > 0 {
319		for ; err == nil && num <= w.MaxFiles; num++ {
320			fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format(format), num, w.suffix)
321			_, err = os.Lstat(fName)
322		}
323	} else {
324		fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", openTime.Format(format), num, w.suffix)
325		_, err = os.Lstat(fName)
326		w.MaxFilesCurFiles = num
327	}
328
329	// return error if the last file checked still existed
330	if err == nil {
331		return fmt.Errorf("Rotate: Cannot find free log number to rename %s", w.Filename)
332	}
333
334	// close fileWriter before rename
335	w.fileWriter.Close()
336
337	// Rename the file to its new found name
338	// even if occurs error,we MUST guarantee to  restart new logger
339	err = os.Rename(w.Filename, fName)
340	if err != nil {
341		goto RESTART_LOGGER
342	}
343
344	err = os.Chmod(fName, os.FileMode(rotatePerm))
345
346RESTART_LOGGER:
347
348	startLoggerErr := w.startLogger()
349	go w.deleteOldLog()
350
351	if startLoggerErr != nil {
352		return fmt.Errorf("Rotate StartLogger: %s", startLoggerErr)
353	}
354	if err != nil {
355		return fmt.Errorf("Rotate: %s", err)
356	}
357	return nil
358}
359
360func (w *fileLogWriter) deleteOldLog() {
361	dir := filepath.Dir(w.Filename)
362	absolutePath, err := filepath.EvalSymlinks(w.Filename)
363	if err == nil {
364		dir = filepath.Dir(absolutePath)
365	}
366	filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
367		defer func() {
368			if r := recover(); r != nil {
369				fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r)
370			}
371		}()
372
373		if info == nil {
374			return
375		}
376		if w.Hourly {
377			if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) {
378				if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
379					strings.HasSuffix(filepath.Base(path), w.suffix) {
380					os.Remove(path)
381				}
382			}
383		} else if w.Daily {
384			if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) {
385				if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
386					strings.HasSuffix(filepath.Base(path), w.suffix) {
387					os.Remove(path)
388				}
389			}
390		}
391		return
392	})
393}
394
395// Destroy close the file description, close file writer.
396func (w *fileLogWriter) Destroy() {
397	w.fileWriter.Close()
398}
399
400// Flush flush file logger.
401// there are no buffering messages in file logger in memory.
402// flush file means sync file from disk.
403func (w *fileLogWriter) Flush() {
404	w.fileWriter.Sync()
405}
406
407func init() {
408	Register(AdapterFile, newFileWriter)
409}
410