diff --git a/logs/conn.go b/logs/conn.go index 5d78467b..1db1a427 100644 --- a/logs/conn.go +++ b/logs/conn.go @@ -17,7 +17,6 @@ package logs import ( "encoding/json" "io" - "log" "net" "time" ) @@ -25,7 +24,7 @@ import ( // connWriter implements LoggerInterface. // it writes messages in keep-live tcp connection. type connWriter struct { - lg *log.Logger + lg *logWriter innerWriter io.WriteCloser ReconnectOnMsg bool `json:"reconnectOnMsg"` Reconnect bool `json:"reconnect"` @@ -43,8 +42,8 @@ func NewConn() Logger { // Init init connection writer with json config. // json config only need key "level". -func (c *connWriter) Init(jsonconfig string) error { - return json.Unmarshal([]byte(jsonconfig), c) +func (c *connWriter) Init(jsonConfig string) error { + return json.Unmarshal([]byte(jsonConfig), c) } // WriteMsg write message in connection. @@ -53,7 +52,7 @@ func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { if level > c.Level { return nil } - if c.neddedConnectOnMsg() { + if c.needToConnectOnMsg() { err := c.connect() if err != nil { return err @@ -64,9 +63,7 @@ func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { defer c.innerWriter.Close() } - msg = formatLogTime(when) + msg - - c.lg.Println(msg) + c.lg.println(when, msg) return nil } @@ -98,11 +95,11 @@ func (c *connWriter) connect() error { } c.innerWriter = conn - c.lg = log.New(conn, "", 0) + c.lg = newLogWriter(conn) return nil } -func (c *connWriter) neddedConnectOnMsg() bool { +func (c *connWriter) needToConnectOnMsg() bool { if c.Reconnect { c.Reconnect = false return true diff --git a/logs/console.go b/logs/console.go index 3781e6cd..05d08a42 100644 --- a/logs/console.go +++ b/logs/console.go @@ -16,7 +16,6 @@ package logs import ( "encoding/json" - "log" "os" "runtime" "time" @@ -47,7 +46,7 @@ var colors = []brush{ // consoleWriter implements LoggerInterface and writes messages to terminal. type consoleWriter struct { - lg *log.Logger + lg *logWriter Level int `json:"level"` Colorful bool `json:"color"` //this filed is useful only when system's terminal supports color } @@ -55,7 +54,7 @@ type consoleWriter struct { // NewConsole create ConsoleWriter returning as LoggerInterface. func NewConsole() Logger { cw := &consoleWriter{ - lg: log.New(os.Stdout, "", 0), + lg: newLogWriter(os.Stdout), Level: LevelDebug, Colorful: true, } @@ -80,12 +79,10 @@ func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error { if level > c.Level { return nil } - msg = formatLogTime(when) + msg if c.Colorful { - c.lg.Println(colors[level](msg)) - } else { - c.lg.Println(msg) + msg = colors[level](msg) } + c.lg.println(when, msg) return nil } diff --git a/logs/file.go b/logs/file.go index 3a042164..9d3f78a0 100644 --- a/logs/file.go +++ b/logs/file.go @@ -53,9 +53,11 @@ type fileLogWriter struct { Level int `json:"level"` Perm os.FileMode `json:"perm"` + + fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix } -// NewFileWriter create a FileLogWriter returning as LoggerInterface. +// newFileWriter create a FileLogWriter returning as LoggerInterface. func newFileWriter() Logger { w := &fileLogWriter{ Filename: "", @@ -89,6 +91,11 @@ func (w *fileLogWriter) Init(jsonConfig string) error { if len(w.Filename) == 0 { return errors.New("jsonconfig must have filename") } + w.suffix = filepath.Ext(w.Filename) + w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix) + if w.suffix == "" { + w.suffix = ".log" + } err = w.startLogger() return err } @@ -118,10 +125,9 @@ func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { if level > w.Level { return nil } - msg = formatLogTime(when) + msg + "\n" - + h, d := formatTimeHeader(when) + msg = string(h) + msg + "\n" if w.Rotate { - d := when.Day() if w.needRotate(len(msg), d) { w.Lock() if w.needRotate(len(msg), d) { @@ -196,7 +202,7 @@ func (w *fileLogWriter) lines() (int, error) { } // DoRotate means it need to write file in new file. -// new file name like xx.2013-01-01.2.log +// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size) func (w *fileLogWriter) doRotate(logTime time.Time) error { _, err := os.Lstat(w.Filename) if err != nil { @@ -206,13 +212,13 @@ func (w *fileLogWriter) doRotate(logTime time.Time) error { // Find the next available number num := 1 fName := "" - suffix := filepath.Ext(w.Filename) - filenameOnly := strings.TrimSuffix(w.Filename, suffix) - if suffix == "" { - suffix = ".log" - } - for ; err == nil && num <= 999; num++ { - fName = filenameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, suffix) + if w.MaxLines > 0 || w.MaxSize > 0 { + for ; err == nil && num <= 999; num++ { + fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, w.suffix) + _, err = os.Lstat(fName) + } + } else { + fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, logTime.Format("2006-01-02"), w.suffix) _, err = os.Lstat(fName) } // return error if the last file checked still existed @@ -250,7 +256,8 @@ func (w *fileLogWriter) deleteOldLog() { }() if !info.IsDir() && info.ModTime().Unix() < (time.Now().Unix()-60*60*24*w.MaxDays) { - if strings.HasPrefix(filepath.Base(path), filepath.Base(w.Filename)) { + if strings.HasPrefix(filepath.Base(path), w.fileNameOnly) && + strings.HasSuffix(filepath.Base(path), w.suffix) { os.Remove(path) } } diff --git a/logs/log.go b/logs/log.go index 428e8e50..c6ba3dc3 100644 --- a/logs/log.go +++ b/logs/log.go @@ -412,45 +412,3 @@ func (bl *BeeLogger) flush() { l.Flush() } } - -func formatLogTime(when time.Time) string { - y, mo, d := when.Date() - h, mi, s := when.Clock() - //len(2006/01/02 15:03:04)==19 - var buf [20]byte - t := 3 - for y >= 10 { - p := y / 10 - buf[t] = byte('0' + y - p*10) - y = p - t-- - } - buf[0] = byte('0' + y) - buf[4] = '/' - if mo > 9 { - buf[5] = '1' - buf[6] = byte('0' + mo - 9) - } else { - buf[5] = '0' - buf[6] = byte('0' + mo) - } - buf[7] = '/' - t = d / 10 - buf[8] = byte('0' + t) - buf[9] = byte('0' + d - t*10) - buf[10] = ' ' - t = h / 10 - buf[11] = byte('0' + t) - buf[12] = byte('0' + h - t*10) - buf[13] = ':' - t = mi / 10 - buf[14] = byte('0' + t) - buf[15] = byte('0' + mi - t*10) - buf[16] = ':' - t = s / 10 - buf[17] = byte('0' + t) - buf[18] = byte('0' + s - t*10) - buf[19] = ' ' - - return string(buf[0:]) -} diff --git a/logs/logger.go b/logs/logger.go new file mode 100644 index 00000000..323c41c5 --- /dev/null +++ b/logs/logger.go @@ -0,0 +1,80 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +package logs + +import ( + "io" + "sync" + "time" +) + +type logWriter struct { + sync.Mutex + writer io.Writer +} + +func newLogWriter(wr io.Writer) *logWriter { + return &logWriter{writer: wr} +} + +func (lg *logWriter) println(when time.Time, msg string) { + lg.Lock() + h, _ := formatTimeHeader(when) + lg.writer.Write(append(append(h, msg...), '\n')) + lg.Unlock() +} + +func formatTimeHeader(when time.Time) ([]byte, int) { + y, mo, d := when.Date() + h, mi, s := when.Clock() + //len(2006/01/02 15:03:04)==19 + var buf [20]byte + t := 3 + for y >= 10 { + p := y / 10 + buf[t] = byte('0' + y - p*10) + y = p + t-- + } + buf[0] = byte('0' + y) + buf[4] = '/' + if mo > 9 { + buf[5] = '1' + buf[6] = byte('0' + mo - 9) + } else { + buf[5] = '0' + buf[6] = byte('0' + mo) + } + buf[7] = '/' + t = d / 10 + buf[8] = byte('0' + t) + buf[9] = byte('0' + d - t*10) + buf[10] = ' ' + t = h / 10 + buf[11] = byte('0' + t) + buf[12] = byte('0' + h - t*10) + buf[13] = ':' + t = mi / 10 + buf[14] = byte('0' + t) + buf[15] = byte('0' + mi - t*10) + buf[16] = ':' + t = s / 10 + buf[17] = byte('0' + t) + buf[18] = byte('0' + s - t*10) + buf[19] = ' ' + + return buf[0:], d +} diff --git a/logs/mulitfile.go b/logs/mulitfile.go new file mode 100644 index 00000000..cf22f7a8 --- /dev/null +++ b/logs/mulitfile.go @@ -0,0 +1,116 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "encoding/json" + "time" +) + +// A filesLogWriter manages several fileLogWriter +// filesLogWriter will write logs to the file in json configuration and write the same level log to correspond file +// means if the file name in configuration is project.log filesLogWriter will create project.error.log/project.debug.log +// and write the error-level logs to project.error.log and write the debug-level logs to project.debug.log +// the rotate attribute also acts like fileLogWriter +type mulitFileLogWriter struct { + writers [LevelDebug + 1 + 1]*fileLogWriter // the last one for fullLogWriter + fullLogWriter *fileLogWriter + Separate []string `json:"separate"` +} + +var levelNames = [...]string{"emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"} + +// Init file logger with json config. +// jsonConfig like: +// { +// "filename":"logs/beego.log", +// "maxLines":0, +// "maxsize":0, +// "daily":true, +// "maxDays":15, +// "rotate":true, +// "perm":0600, +// "separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"], +// } + +func (f *mulitFileLogWriter) Init(config string) error { + writer := newFileWriter().(*fileLogWriter) + err := writer.Init(config) + if err != nil { + return err + } + f.fullLogWriter = writer + f.writers[LevelDebug+1] = writer + + //unmarshal "separate" field to f.Separate + json.Unmarshal([]byte(config), f) + + jsonMap := map[string]interface{}{} + json.Unmarshal([]byte(config), &jsonMap) + + for i := LevelEmergency; i < LevelDebug+1; i++ { + for _, v := range f.Separate { + if v == levelNames[i] { + jsonMap["filename"] = f.fullLogWriter.fileNameOnly + "." + levelNames[i] + f.fullLogWriter.suffix + jsonMap["level"] = i + bs, _ := json.Marshal(jsonMap) + writer = newFileWriter().(*fileLogWriter) + writer.Init(string(bs)) + f.writers[i] = writer + } + } + } + + return nil +} + +func (f *mulitFileLogWriter) Destroy() { + for i := 0; i < len(f.writers); i++ { + if f.writers[i] != nil { + f.writers[i].Destroy() + } + } +} + +func (f *mulitFileLogWriter) WriteMsg(when time.Time, msg string, level int) error { + if f.fullLogWriter != nil { + f.fullLogWriter.WriteMsg(when, msg, level) + } + for i := 0; i < len(f.writers)-1; i++ { + if f.writers[i] != nil { + if level == f.writers[i].Level { + f.writers[i].WriteMsg(when, msg, level) + } + } + } + return nil +} + +func (f *mulitFileLogWriter) Flush() { + for i := 0; i < len(f.writers); i++ { + if f.writers[i] != nil { + f.writers[i].Flush() + } + } +} + +// newFilesWriter create a FileLogWriter returning as LoggerInterface. +func newFilesWriter() Logger { + return &mulitFileLogWriter{} +} + +func init() { + Register("mulitfile", newFilesWriter) +} diff --git a/logs/mulitfile_test.go b/logs/mulitfile_test.go new file mode 100644 index 00000000..5e1e3f06 --- /dev/null +++ b/logs/mulitfile_test.go @@ -0,0 +1,78 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "bufio" + "os" + "strconv" + "strings" + "testing" +) + +func TestFiles_1(t *testing.T) { + log := NewLogger(10000) + log.SetLogger("mulitfile", `{"filename":"test.log","separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"]}`) + log.Debug("debug") + log.Informational("info") + log.Notice("notice") + log.Warning("warning") + log.Error("error") + log.Alert("alert") + log.Critical("critical") + log.Emergency("emergency") + fns := []string{""} + fns = append(fns, levelNames[0:]...) + name := "test" + suffix := ".log" + for _, fn := range fns { + + file := name + suffix + if fn != "" { + file = name + "." + fn + suffix + } + f, err := os.Open(file) + if err != nil { + t.Fatal(err) + } + b := bufio.NewReader(f) + lineNum := 0 + lastLine := "" + for { + line, _, err := b.ReadLine() + if err != nil { + break + } + if len(line) > 0 { + lastLine = string(line) + lineNum++ + } + } + var expected = 1 + if fn == "" { + expected = LevelDebug + 1 + } + if lineNum != expected { + t.Fatal(file, "has", lineNum, "lines not "+strconv.Itoa(expected)+" lines") + } + if lineNum == 1 { + if !strings.Contains(lastLine, fn) { + t.Fatal(file + " " + lastLine + " not contains the log msg " + fn) + } + } + os.Remove(file) + } + +}