在使用Go語言的時候,發(fā)現標準庫的日志log功能很初級拉鹃。
log.Println(i)
log.Printf(format, i...)
log.Fatalln(i)
log.Panic()
...
基本也就圍繞上面幾個方法來做日志處理鲫忍。于是我上Github查找第三方庫。在看了幾個log庫之后坝辫,覺得不是滿意射亏。我打算自己嘗試實現log日志組件。
最后實現log的使用如下:
// 標準日志
l := logs.NewLogger()
l.SetLogLevel(logs.WARN) //設置日志輸出等級
l.Debug("Debug")
l.Info("Info")
l.Notice("Notice")
l.Warn(100)
l.Error("Error")
l.Critical("Critical")
l.Fatal("Fatal")
// 保存到本地log文件的日志
l := logs.NewRotateFileLogger("/Users/xxx/logs") // 設置log日志文件存放路徑
l.SetLogLevel(logs.INFO) //設置日志輸出等級
l.SetNewFileGapTime(time.Hour) // 設置每個小時創(chuàng)建新的log文件
l.Notice("Notice")
l.Warn(100)
l.Error("Error")
l.Critical("Critical")
l.Fatal("Fatal")
性能上通過Benchmark分析輸出一個Hello
平均每次執(zhí)行時間:4177ns/p
內存使用情況:168 B/op 和 7 allocs/op
完全可以應對每秒上萬的輸出及舍。性能達到要求了窟绷。
設計思考
我對組件一直有這樣的思路,
組件要易于上手攘残,減少學習成本。盡量減少不必要的概念和抽象遗契。
初始的組件能滿足最常用的使用場景病曾,開箱即可用。滿足80%的情況知态。同時又易于拓展负敏,特別需求可以簡單通過組件的拓展來封裝贡茅。
組件API精簡顶考,設計上更符合人們對組件直觀的認知妖泄。
開發(fā)前
對日志組件的需求,大部分情況無非一幾點
- 帶有Info蹈胡、Warn罚渐、Error等等的不同等級的輸出。
- 可以設置輸出等級荷并。
- 輸出內容格式可以自定義。
- 支持log文件的寫入翩伪。
- 最好也支持過濾Filter輸出的功能谈息。
設計API
初定的API
l.SetLogLevel(level) //設置日志輸出等級
l.Debug(...)
l.Info(...)
l.Notice(...)
l.Warn(...)
...
l.SetFormat(...) //設置輸出格式
前面的幾個API比較好理解,但是在設計輸出格式SetFormat的時候囊颅,我查看了幾個第三方庫,特別容易在這里引入概念和抽象層盲憎。設置還有封裝出格式化結構的struct來定義格式的胳挎。這樣的設計明顯不符合我之前說的設計思考的幾點,于是我查看了官方的log慕爬。官方的log很簡樸但是很好理解,一個format字符串+對應format字符串格式的內容磅甩。例如格式%s-%s 對應2017/11/26 和Hello姥卢。這種方式開發(fā)者都熟悉,比較適合僧叉。所以我現在了官方的方式來自定義格式棺榔。
可是用一個SetFormat來傳入參數定義格式其實很難封裝得好,因為格式和前面的Info(data)有點不好封裝症歇,格式和data可以對應,但是擴展很弱宛蚓。
我想輸出是2017/11/26 Hello德频,定義好格式后缩幸,data不能簡單的是Hello,Info(data) 這里的data就要自己再加入時間2017/11/26钞护。
于是我選擇了用傳入函數來定義格式輸出爆办。
logFormatFunc func(logType LogType, i interface{}) (string, []interface{}, bool)
logType標記輸出的等級,i表示輸出的日志余佃,
函數返回(string, []interface{},bool)
string是格式 例如%s-%s。 []interface{}表示返回格式的內容爆土。
bool主要是滿足做Filter過濾器
好了 先看看Logger默認的格式定義函數
func (l *Logger) DefaultLogFormatFunc(logType LogType, i interface{}) (string, []interface{}, bool){
format := "\033["+ logTypesColors[logType] + "m%s [%s] %s \033[0m\n"
layout :="2006-01-02 15:04:05.999"
formatTime := time.Now().Format(layout)
if len(formatTime) != len(layout) {
// 可能出現結尾是0被省略 如 2006-01-02 15:04:05.9 2006-01-02 15:04:05.99步势,補上
formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
}
values := make([]interface{}, 3)
values[0] = logTypeStrings[logType]
values[1] = formatTime
values[2] = fmt.Sprint(i)
return format, values, true
}
其實就是格式定義是%s [%s] %s
返回[]interface{} 對應格式的 3個%s。
編寫Logger
提供Info(...) Warn(...) Error(...) 和 SetLogLevel(level) 再提供了SetLoggerFormat設置自定義格式輸出等等功能盅抚。
接口少但易于拓展倔矾。
通過SetLoggerFormat 我拓展實現了log文件的RotateFileLogger
RotateFileLogger的格式函數如下:
func (l *RotateFileLogger) DefaultLogFormatFunc(logType LogType, i interface{}) (string, []interface{}, bool){
format := "%s [%s] %s\n"
now := time.Now()
gapTime := now.Sub(l.lastFileTime)
if gapTime > l.newFileGapTime && l.newFileGapTime > 0{
l.file.Close()
rate := int(int64(gapTime) / int64(l.newFileGapTime))
l.lastFileTime = l.lastFileTime.Add(l.newFileGapTime * time.Duration(rate))
file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
if err != nil{
file.Close()
panic(err)
}
l.file = file
l.out = file
}
layout :="2006-01-02 15:04:05.999"
formatTime := now.Format(layout)
defer func() {
e := recover()
if e!=nil{
panic(debug.Stack())
}
}()
if len(formatTime) != len(layout) {
// 對于如果是 出現2006-01-02 15:04:05.99 適配處理 成2006-01-02 15:04:05.990
formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
}
values := make([]interface{}, 3)
values[0] = GetLogTypeString(logType)
values[1] = formatTime
values[2] = fmt.Sprint(i)
return format, values, true
}
源碼 (2個文件)
logger.go
package logs
import (
"sync"
"io"
"fmt"
"time"
"os"
"strings"
)
type LogType int
const (
DEBUG = LogType(0)
INFO = LogType(1)
NOTICE = LogType(2)
WARN = LogType(3)
ERROR = LogType(4)
CRITICAL = LogType(5)
FATAL = LogType(6)
)
var logTypeStrings = func() []string{
// log 類型對應的 名稱字符串破讨,用于輸出,所以統(tǒng)一了長度提陶,故 DEBUG 為 "DEBUG..." 和 "CRITICAL"等長
types :=[]string{"DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "CRITICAL", "FATAL"}
maxTypeLen := 0
for _, t := range types{
if len(t) > maxTypeLen {
maxTypeLen = len(t)
}
}
for index, t := range types{
typeLen := len(t)
if typeLen < maxTypeLen{
types[index] += strings.Repeat(".", maxTypeLen - typeLen)
}
}
return types
}()
var logTypesColors = []string{"0;35", "1;36", "1;37", "0;33", "1;31", "1;31", "1;31"}
/*
創(chuàng)建 Logger
*/
func NewLogger() *Logger{
logger := &Logger{}
logger.Init()
return logger
}
func GetLogTypeString(t LogType) string{
return logTypeStrings[t]
}
/*
Logger 日志
l := NewLogger()
l.Info("hello")
l.Warn(1)
輸入格式可以通過 SetLoggerFormat 設置隙笆。默認輸出格式定義在 見Logger的DefaultLogFormatFunc
可以通過 SetLogLevel 設置輸出等級。
*/
type Logger struct {
mu sync.Mutex
out io.Writer
logFormatFunc func(logType LogType, i interface{}) (string, []interface{}, bool)
logLevel LogType
}
func (l *Logger) Init() {
l.mu.Lock()
defer l.mu.Unlock()
l.logFormatFunc = l.DefaultLogFormatFunc
l.out = os.Stdout
l.logLevel = DEBUG
}
func (l* Logger) SetLogLevel(logType LogType) {
l.mu.Lock()
defer l.mu.Unlock()
l.logLevel = logType
}
func (l* Logger) GetLogLevel() LogType{
l.mu.Lock()
defer l.mu.Unlock()
return l.logLevel
}
// 設置格式化log輸出函數
// 函數返回 format 和 對應格式 []interface{}
func (l *Logger) SetLoggerFormat(formatFunc func(logType LogType, i interface{}) (string, []interface{}, bool)) {
l.mu.Lock()
defer l.mu.Unlock()
l.logFormatFunc = formatFunc
}
// 輸出信息
func (l *Logger) Debug(i interface{}) {
l.log(DEBUG, i)
}
func (l *Logger) Info(i interface{}){
l.log(INFO, i)
}
func (l *Logger) Notice(i interface{}){
l.log(NOTICE, i)
}
func (l *Logger) Warn(i interface{}) {
l.log(WARN, i)
}
func (l *Logger) Error(i interface{}) {
l.log(ERROR, i)
}
func (l *Logger) Critical(i interface{}) {
l.log(CRITICAL, i)
}
func (l *Logger) Fatal(i interface{}) {
l.log(FATAL, i)
}
func (l *Logger) DefaultLogFormatFunc(logType LogType, i interface{}) (string, []interface{}, bool){
format := "\033["+ logTypesColors[logType] + "m%s [%s] %s \033[0m\n"
layout :="2006-01-02 15:04:05.999"
formatTime := time.Now().Format(layout)
if len(formatTime) != len(layout) {
// 可能出現結尾是0被省略 如 2006-01-02 15:04:05.9 2006-01-02 15:04:05.99,補上
formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
}
values := make([]interface{}, 3)
values[0] = logTypeStrings[logType]
values[1] = formatTime
values[2] = fmt.Sprint(i)
return format, values, true
}
func (l *Logger) log(logType LogType, i interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
if l.logLevel > logType{
return
}
format, data, isLog := l.logFormatFunc(logType, i)
if !isLog{ return}
_, err := fmt.Fprintf(l.out, format, data...)
if err !=nil{
panic(err)
}
}
file_logger.go
package logs
import (
"os"
"time"
"fmt"
"runtime/debug"
)
/*
*
*/
func NewRotateFileLogger(dir string) *RotateFileLogger{
logger := &RotateFileLogger{}
logger.Init(dir)
return logger
}
// 一段時間自動創(chuàng)建新的log文件
// 如果在間隔時間無日志剪决,此間隔不會創(chuàng)建log文件
type RotateFileLogger struct {
Logger
file *os.File // 正在操作文件
dirPath string // logs 文件所在文件夾
fileNameFormatFunc func(t time.Time) string // 獲取文件名稱格式
newFileGapTime time.Duration // 創(chuàng)建新log的間隔時間
lastFileTime time.Time // 上次創(chuàng)建文件檀训,文件對應時間(依據間隔時間,不是真實創(chuàng)建時間)
}
func (l * RotateFileLogger) Init(dir string) {
l.Logger.Init()
l.mu.Lock()
defer l.mu.Unlock()
l.fileNameFormatFunc = l.DefaultFileNameFormat
l.logFormatFunc = l.DefaultLogFormatFunc
l.newFileGapTime = 0
l.lastFileTime = time.Now()
l.dirPath = dir
file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
if err != nil{
panic(err)
return
}
l.file = file
l.out = file
}
func (l * RotateFileLogger) SetNewFileGapTime(gapTime time.Duration) {
l.mu.Lock()
defer l.mu.Unlock()
l.newFileGapTime = gapTime
}
// 默認 創(chuàng)建格式化的文件名. fileTime 不一定是創(chuàng)建文件的時間
func (l * RotateFileLogger) DefaultFileNameFormat(fileTime time.Time) string{
// tips: linux 不支持 2006-01-02 15:04:05.999 ":"名稱
// tips: os x 2006/01-02 15-04-05-999 帶有"/"報no such file or directory
layout :="2006-01-02 15-04-05-999"
formatTime := fileTime.Format(layout)
if len(formatTime) != len(layout) {
// 對于如果是 出現2006-01-02 15:04:05.99 適配處理 成2006-01-02 15:04:05.990
formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
}
return formatTime + ".log"
}
func (l * RotateFileLogger) createLogFile(filename string) (*os.File, error){
if len(l.dirPath) != 0{
filename = l.dirPath + "/" + filename
err:=os.MkdirAll(l.dirPath, 0777)
if err != nil{
panic(err)
}
}
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil{
return nil, err
}
return file, nil
}
func (l *RotateFileLogger) DefaultLogFormatFunc(logType LogType, i interface{}) (string, []interface{}, bool){
format := "%s [%s] %s\n"
now := time.Now()
gapTime := now.Sub(l.lastFileTime)
if gapTime > l.newFileGapTime && l.newFileGapTime > 0{
l.file.Close()
rate := int(int64(gapTime) / int64(l.newFileGapTime))
l.lastFileTime = l.lastFileTime.Add(l.newFileGapTime * time.Duration(rate))
file, err := l.createLogFile(l.fileNameFormatFunc(l.lastFileTime))
if err != nil{
file.Close()
panic(err)
}
l.file = file
l.out = file
}
layout :="2006-01-02 15:04:05.999"
formatTime := now.Format(layout)
defer func() {
e := recover()
if e!=nil{
panic(debug.Stack())
}
}()
if len(formatTime) != len(layout) {
// 對于如果是 出現2006-01-02 15:04:05.99 適配處理 成2006-01-02 15:04:05.990
formatTime += ".000"[4 - (len(layout) - len(formatTime)):4]
}
values := make([]interface{}, 3)
values[0] = GetLogTypeString(logType)
values[1] = formatTime
values[2] = fmt.Sprint(i)
return format, values, true
}