diff --git a/README.md b/README.md index 3ad7690..bc356f3 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,8 @@ request.Session().Set("user", "alice") The logger provides the eight logging levels defined in [RFC 5424]( https://tools.ietf.org/html/rfc5424 ): **emergency**, **alert**, **critical**, **error**, **warning**, **notice**, **info** and **debug**. +#### Basic Usage + ```go import "github.com/thinkoner/thinkgo/log" @@ -410,6 +412,24 @@ log.Alert("log with Alert") log.Emerg("log with Emerg") ``` +#### Log Storage + +Out of the box, ThinkGo supports writing log information to `daily` files, the `console`. + +For example, if you wish to use `daily` log files, you can do this: + +```go +import ( + "github.com/thinkoner/thinkgo/log" + "github.com/thinkoner/thinkgo/log/handler" + "github.com/thinkoner/thinkgo/log/record" +) + +fh := handler.NewFileHandler("path/to/thinkgo.log", record.INFO) + +log.GetLogger().PushHandler(fh) +``` + ## Cache ThinkGo Cache Currently supports redis, memory, and can customize the store adapter. diff --git a/log/handler/console.go b/log/handler/console.go index b1ec6c7..88024e9 100644 --- a/log/handler/console.go +++ b/log/handler/console.go @@ -4,7 +4,6 @@ import ( "os" "sync" - "github.com/thinkoner/thinkgo/log/formatter" "github.com/thinkoner/thinkgo/log/record" ) @@ -18,7 +17,7 @@ func newBrush(color string) brush { } } -var colors = map[int]brush{ +var colors = map[record.Level]brush{ record.EMERGENCY: newBrush("1;41"), // Emergency Red background record.ALERT: newBrush("1;35"), // Alert purple record.CRITICAL: newBrush("1;34"), // Critical blue @@ -30,15 +29,16 @@ var colors = map[int]brush{ } type ConsoleHandler struct { + Handler sync.Mutex - level int - formatter formatter.Formatter - bubble bool + level record.Level + + bubble bool } -func NewConsoleHandler() *ConsoleHandler { +func NewConsoleHandler(level record.Level) *ConsoleHandler { return &ConsoleHandler{ - level: record.DEBUG, + level: level, bubble: true, } } @@ -67,21 +67,3 @@ func (h *ConsoleHandler) write(r record.Record) { message := colors[r.Level](r.Formatted) os.Stdout.Write(append([]byte(message))) } - -// GetFormatter Gets the formatter. -func (h *ConsoleHandler) GetFormatter() formatter.Formatter { - if h.formatter == nil { - h.formatter = h.getDefaultFormatter() - } - return h.formatter -} - -// SetFormatter Sets the formatter. -func (h *ConsoleHandler) SetFormatter(f formatter.Formatter) *ConsoleHandler { - h.formatter = f - return h -} - -func (h *ConsoleHandler) getDefaultFormatter() formatter.Formatter { - return formatter.NewLineFormatter() -} diff --git a/log/handler/file.go b/log/handler/file.go new file mode 100644 index 0000000..c6fd579 --- /dev/null +++ b/log/handler/file.go @@ -0,0 +1,83 @@ +package handler + +import ( + "os" + "path" + "strings" + "sync" + "time" + + "github.com/thinkoner/thinkgo/log/record" +) + +type FileHandler struct { + Handler + sync.Mutex + level record.Level + bubble bool + filename string + + filenameFormat string + dateFormat string + timedFilename string +} + +func NewFileHandler(filename string, level record.Level) *FileHandler { + h := &FileHandler{ + level: level, + bubble: true, + filename: filename, + filenameFormat: "{filename}-{date}", + dateFormat: "2006-01-02", + } + h.timedFilename = h.GetTimedFilename() + return h +} + +// IsHandling Checks whether the given record will be handled by this handler. +func (h *FileHandler) IsHandling(r record.Record) bool { + return r.Level >= h.level +} + +// Handle Handles a record. +func (h *FileHandler) Handle(r record.Record) bool { + if !h.IsHandling(r) { + return false + } + + r.Formatted = h.GetFormatter().Format(r) + + h.write(r) + + return false == h.bubble +} + +// SetLevel Sets minimum logging level at which this handler will be triggered. +func (h *FileHandler) SetLevel(level record.Level) { + h.level = level +} + +func (h *FileHandler) write(r record.Record) { + h.Lock() + defer h.Unlock() + file, _ := os.OpenFile(h.timedFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + defer file.Close() + file.Write([]byte(r.Formatted)) +} + +// GetTimedFilename Gets the Timed filename. +func (h *FileHandler) GetTimedFilename() string { + dirname := path.Dir(h.filename) + filename := path.Base(h.filename) + fileExt := path.Ext(h.filename) + filename = strings.TrimSuffix(filename, fileExt) + + timedFilename := strings.Replace(path.Join(dirname, h.filenameFormat), "{filename}", filename, -1) + timedFilename = strings.Replace(timedFilename, "{date}", time.Now().Local().Format(h.dateFormat), -1) + + if len(fileExt) > 0 { + timedFilename = timedFilename + fileExt + } + + return timedFilename +} diff --git a/log/handler/file_test.go b/log/handler/file_test.go new file mode 100644 index 0000000..d1e2245 --- /dev/null +++ b/log/handler/file_test.go @@ -0,0 +1,46 @@ +package handler + +import ( + "io/ioutil" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/thinkoner/thinkgo/log/record" +) + +func TestNewFileHandler(t *testing.T) { + filename := path.Join(os.TempDir(), "thinkgo.log") + + h := NewFileHandler(filename, record.DEBUG) + filename = h.GetTimedFilename() + + os.Remove(filename) + + message := "Log write to file" + r := getRecord(message) + h.Handle(r) + + b, err := ioutil.ReadFile(filename) + if err != nil { + t.Error(err) + } + content := string(b) + + if !strings.Contains(content, message) { + t.Error("test FileHandler error") + } + +} + +func getRecord(message string) record.Record { + return record.Record{ + Level: 200, + Message: message, + LevelName: "INFO", + Channel: "testing", + Datetime: time.Now(), + } +} diff --git a/log/handler/handler.go b/log/handler/handler.go new file mode 100644 index 0000000..a589d89 --- /dev/null +++ b/log/handler/handler.go @@ -0,0 +1,29 @@ +package handler + +import ( + "github.com/thinkoner/thinkgo/log/formatter" + "github.com/thinkoner/thinkgo/log/record" +) + +type Handler struct { + formatter formatter.Formatter + level record.Level +} + +// GetFormatter Gets the formatter. +func (h *Handler) GetFormatter() formatter.Formatter { + if h.formatter == nil { + h.formatter = h.getDefaultFormatter() + } + return h.formatter +} + +// SetFormatter Sets the formatter. +func (h *Handler) SetFormatter(f formatter.Formatter) *Handler { + h.formatter = f + return h +} + +func (h *Handler) getDefaultFormatter() formatter.Formatter { + return formatter.NewLineFormatter() +} diff --git a/log/log.go b/log/log.go index d1f74cc..819447b 100644 --- a/log/log.go +++ b/log/log.go @@ -1,9 +1,13 @@ package log +import ( + "github.com/thinkoner/thinkgo/log/record" +) + var logger *Logger func init() { - logger = NewLogger("develop") + logger = NewLogger("develop", record.DEBUG) } // Debug Adds a log record at the DEBUG level. @@ -45,3 +49,13 @@ func Alert(format string, v ...interface{}) (bool, error) { func Emerg(format string, v ...interface{}) (bool, error) { return logger.Emerg(format, v...) } + +// GetLogger Get the default Logger +func GetLogger() *Logger { + return logger +} + +// SetLogger Set the default Logger +func SetLogger(l *Logger) { + logger = l +} diff --git a/log/log_test.go b/log/log_test.go index 2038ef4..b0c6220 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -1,7 +1,15 @@ package log import ( + "errors" + "io/ioutil" + "os" + "path" + "strings" "testing" + + "github.com/thinkoner/thinkgo/log/handler" + "github.com/thinkoner/thinkgo/log/record" ) func TestLog(t *testing.T) { @@ -14,3 +22,43 @@ func TestLog(t *testing.T) { Alert("log with Alert") Emerg("log with Emerg") } + +func TestLogWithFileHandler(t *testing.T) { + + filename := path.Join(os.TempDir(), "thinkgo.log") + + h := handler.NewFileHandler(filename, record.INFO) + + l := NewLogger("testing", record.INFO) + l.PushHandler(h) + + filename = h.GetTimedFilename() + + os.Remove(filename) + + message := "Log write to file" + + l.Debug(message) + + _, err := ioutil.ReadFile(filename) + if err == nil { + t.Error(errors.New("test FileHandler error")) + } + + + h.SetLevel(record.DEBUG) + l = NewLogger("testing", record.DEBUG) + l.PushHandler(h) + l.Debug(message) + + b, err := ioutil.ReadFile(filename) + if err != nil { + t.Error(errors.New("test FileHandler error")) + } + content := string(b) + + if !strings.Contains(content, message) { + t.Error("test FileHandler error") + } + +} diff --git a/log/logger.go b/log/logger.go index 5bd63eb..21b09f7 100644 --- a/log/logger.go +++ b/log/logger.go @@ -17,13 +17,16 @@ type Handler interface { type Logger struct { name string + level record.Level handlers *list.List } -func NewLogger(name string) *Logger { +// NewLogger New a Logger instance +func NewLogger(name string, level record.Level) *Logger { logger := &Logger{ name: name, handlers: list.New(), + level: level, } return logger } @@ -71,9 +74,10 @@ func (logger *Logger) GetHandlers() []Handler { return handler } -func (logger *Logger) AddRecord(level int, format string, v ...interface{}) (bool, error) { +// AddRecord Adds a log record. +func (logger *Logger) AddRecord(level record.Level, format string, v ...interface{}) (bool, error) { if logger.handlers.Len() == 0 { - logger.PushHandler(handler.NewConsoleHandler()) + logger.PushHandler(handler.NewConsoleHandler(logger.level)) } levelName, err := GetLevelName(level) @@ -156,7 +160,7 @@ func (logger *Logger) Emerg(format string, v ...interface{}) (bool, error) { } // Gets the name of the logging level. -func GetLevelName(level int) (string, error) { +func GetLevelName(level record.Level) (string, error) { levels := record.GetLevels() l, ok := levels[level] if !ok { diff --git a/log/record/record.go b/log/record/record.go index f9cc6b7..8bd6a23 100644 --- a/log/record/record.go +++ b/log/record/record.go @@ -2,6 +2,8 @@ package record import "time" +type Level int + const ( // Detailed debug information DEBUG = 100 @@ -22,7 +24,7 @@ const ( ) // Logging levels from syslog protocol defined in RFC 5424 -var levels = map[int]string{ +var levels = map[Level]string{ DEBUG: "DEBUG", INFO: "INFO", NOTICE: "NOTICE", @@ -34,7 +36,7 @@ var levels = map[int]string{ } type Record struct { - Level int + Level Level Message string LevelName string Channel string @@ -43,6 +45,6 @@ type Record struct { } // GetLevels returns levels map -func GetLevels() map[int]string { +func GetLevels() map[Level]string { return levels }