Go 自定义日志库
日志库
自定义一个日志库。
知识储备
runtime.Caller()
该方法能够获取到打印的位置,文件的信息,行数等。
以下是该方法的使用,不必纠结太多,照着用就行。
唯一注意的是caller()
中值的放入,该值会影响行数的显示,多测试几遍你就大概明白了。
package main
import (
"fmt"
"runtime"
"path/filepath"
)
func f1() {
pc, file, line, ok := runtime.Caller(1) // 当被其他函数调用,则设置为1. 当无其他函数调用则设置为0
if !ok {
fmt.Println("获取信息时出错")
return
}
funcName := runtime.FuncForPC(pc).Name() // 获取调用该函数的函数名字
fmt.Println(funcName) // 打印调用者姓名
fmt.Println(filepath.Base(file)) // 打印文件
fmt.Println(line) // 打印行
}
func main() {
f1()
}
errors.New()
该方法用于在函数中返回一个error
对象时,添加一个新错误。
package main
import (
"errors"
"fmt"
)
func f1() (string, error) {
return "哈哈哈", errors.New("新错误")
}
func main() {
str, err := f1()
fmt.Println(err) // 新错误
fmt.Println(str) // 哈哈哈
}
具体代码
myLogger.go
package mylogger
import (
"errors"
"fmt"
"path/filepath"
"runtime"
"strings"
)
// LogLevel 日志级别 (自类型)
type LogLevel uint16
// Logger 接口
type Logger interface {
Debug(format string, args ...interface{})
Trace(format string, args ...interface{})
Info(format string, args ...interface{})
Warning(format string, args ...interface{})
Error(format string, args ...interface{})
Fatal(format string, args ...interface{})
}
// 定义日志级别
const (
UNKNOWN LogLevel = iota
DEBUG
TRACE
INFO
WARNING
ERROR
FATAL
)
func parseLogLevel(s string) (LogLevel, error) {
s = strings.ToLower(s)
switch s {
case "debug":
return DEBUG, nil
case "trace":
return TRACE, nil
case "info":
return INFO, nil
case "warning":
return WARNING, nil
case "error":
return ERROR, nil
case "fatal":
return FATAL, nil
default:
err := errors.New("无效的日志级别")
return UNKNOWN, err
}
}
func getLogString(lv LogLevel) string {
switch lv {
case DEBUG:
return "DEBUG"
case TRACE:
return "TRACE"
case INFO:
return "INFO"
case WARNING:
return "WARNING"
case ERROR:
return "ERROR"
case FATAL:
return "FATAL"
default:
return "DEBUG"
}
}
func getInfo(skip int) (funcName, fileName string, lineNo int) {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
fmt.Printf("runtime.Caller() failed\n")
return
}
funcName = runtime.FuncForPC(pc).Name()
funcName = strings.Split(funcName, ".")[1]
fileName = filepath.Base(file)
lineNo = line
return funcName, fileName, lineNo
}
file.go
package mylogger
import (
"fmt"
"os"
"path/filepath"
"time"
)
// FileLogger 往文件里面写日志相关代码
type FileLogger struct {
level LogLevel
filePath string // 日志文件保存路径
fileName string // 日志文件名
maxFileSize int64 // 最大的文件大小
fileObj *os.File
errFileObj *os.File
}
// NewFileLogger ...
func NewFileLogger(levelStr, fp, fn string, maxSize int64) *FileLogger {
logLevel, err := parseLogLevel(levelStr)
if err != nil {
panic(err)
}
fl := &FileLogger{
level: logLevel,
filePath: fp,
fileName: fn,
maxFileSize: maxSize,
}
err = fl.initFile() // 打开文件,获取文件对象
if err != nil {
panic(err)
}
return fl
}
func (f *FileLogger) initFile() error {
// 创建记录正确的日志文件
fullFileName := filepath.Join(f.filePath, f.fileName)
fileObj, err := os.OpenFile(fullFileName+".log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("open log file faild,err:%v", err)
return err
}
// 创建错误的日志文件
errFileObj, err := os.OpenFile(fullFileName+"_err.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("open err log file faild,err:%v", err)
return err
}
f.fileObj = fileObj
f.errFileObj = errFileObj
return nil
}
func (f *FileLogger) enable(logLevel LogLevel) bool {
return f.level <= logLevel
}
func (f *FileLogger) log(lv LogLevel, format string, args ...interface{}) {
if f.enable(lv) {
msg := fmt.Sprintf(format, args...) // 合并输出
funcName, fileName, lineNo := getInfo(3) // 三层调用
now := time.Now().Format("2006-01-02 03:04:06")
lvStr := getLogString(lv)
if f.checkSize(f.fileObj) {
newFile, err := f.splitFile(f.fileObj)
if err != nil {
return
}
f.fileObj = newFile
}
fmt.Fprintf(f.fileObj, "[%s] [%s] [%s:%s:%d] %s \n", now, lvStr, fileName, funcName, lineNo, msg)
if lv >= ERROR {
if f.checkSize(f.errFileObj) {
newFile, err := f.splitFile(f.errFileObj)
if err != nil {
return
}
f.errFileObj = newFile
}
// 如果记录日志级别大于或等于ERROR,则再记录一份到LogErr的文件中
fmt.Fprintf(f.errFileObj, "[%s] [%s] [%s:%s:%d] %s \n", now, lvStr, fileName, funcName, lineNo, msg)
}
}
}
// Debug ...
func (f *FileLogger) Debug(format string, args ...interface{}) {
f.log(DEBUG, format, args...)
}
// Trace ...
func (f *FileLogger) Trace(format string, args ...interface{}) {
f.log(TRACE, format, args...)
}
// Info ...
func (f *FileLogger) Info(format string, args ...interface{}) {
f.log(INFO, format, args...)
}
// Warning ...
func (f *FileLogger) Warning(format string, args ...interface{}) {
f.log(WARNING, format, args...)
}
// Error ...
func (f *FileLogger) Error(format string, args ...interface{}) {
f.log(ERROR, format, args...)
}
// Fatal ...
func (f *FileLogger) Fatal(format string, args ...interface{}) {
f.log(FATAL, format, args...)
}
// Close 关闭文件资源
func (f *FileLogger) Close() {
f.fileObj.Close()
f.errFileObj.Close()
}
// 获取文件大小,判断是否要进行切割
func (f *FileLogger) checkSize(file *os.File) bool {
fileInfo, err := file.Stat()
if err != nil {
fmt.Printf("get file info failed,err%v\n", err)
return false
}
// 如果当前文件的size大于设定的size,则返回true,否则返回false
return fileInfo.Size() >= f.maxFileSize
}
func (f *FileLogger) splitFile(file *os.File) (*os.File, error) {
nowStr := time.Now().Format("20060102150405000")
fileInfo, err := file.Stat()
if err != nil {
fmt.Printf("get file info failed,err:%v\n", err)
return nil, err
}
logName := filepath.Join(f.filePath, fileInfo.Name())
newlogName := fmt.Sprintf("%s.%s.bak", logName, nowStr)
// 1. 关闭当前文件
file.Close()
// 2. 备份一个 rename
os.Rename(logName, newlogName)
// 3. 打开一个新的日志文件
fileObj, err := os.OpenFile(logName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("open log file failed, err:%v", err)
return nil, err
}
// 4. 将打开的文件赋值给 fl.FileObj
return fileObj, nil
}
consloe.go
package mylogger
import (
"fmt"
"time"
)
// 往终端写日志
// ConsoleLogger ...
type ConsoleLogger struct {
level LogLevel
}
// NewConsoleLogger 构造函数 ...
func NewConsoleLogger(levelStr string) ConsoleLogger {
level, err := parseLogLevel(levelStr)
if err != nil {
panic(err)
}
return ConsoleLogger{
level: level,
}
}
func (c ConsoleLogger) enable(logLevel LogLevel) bool {
return c.level <= logLevel
}
func (c ConsoleLogger) log(lv LogLevel, format string, args ...interface{}) {
if c.enable(lv) {
msg := fmt.Sprintf(format, args...) // 合并输出
funcName, fileName, lineNo := getInfo(3) // 三层调用
now := time.Now().Format("2006-01-02 03:04:06")
lvStr := getLogString(lv)
fmt.Printf("[%s] [%s] [%s:%s:%d] %s \n", now, lvStr, fileName, funcName, lineNo, msg)
}
}
// Debug ...
func (c ConsoleLogger) Debug(format string, args ...interface{}) {
c.log(DEBUG, format, args...)
}
// TRACE ...
func (c ConsoleLogger) Trace(format string, args ...interface{}) {
c.log(TRACE, format, args...)
}
// Info ...
func (c ConsoleLogger) Info(format string, args ...interface{}) {
c.log(INFO, format, args...)
}
// Warning ...
func (c ConsoleLogger) Warning(format string, args ...interface{}) {
c.log(WARNING, format, args...)
}
// Error ...
func (c ConsoleLogger) Error(format string, args ...interface{}) {
c.log(ERROR, format, args...)
}
// Fatal ...
func (c ConsoleLogger) Fatal(format string, args ...interface{}) {
c.log(FATAL, format, args...)
}
使用案例
日志库分为往屏幕打印与往文件写入两种。
通过构造函数实例化不同的对象然后进行操作,支持格式化。
支持文件切割,可指定每个日志文件的大小。
package main
import (
mylogger "yunya.com/module"
)
func main() {
fileLog := mylogger.NewFileLogger("debug", "./", "test", 3*1024) // 向文件打印
consoleLog := mylogger.NewConsoleLogger("debug")
for {
fileLog.Debug("Debug%v", "试试")
fileLog.Info("Info")
fileLog.Warning("Warning")
fileLog.Error("Error")
fileLog.Fatal("Fatal")
consoleLog.Debug("Debug")
}
}
异步写入
以下是对写入文件的代码进行优化。会自动开一个goroutine来写入内容。
package mylogger import ( "fmt" "os" "path/filepath" "time" ) // FileLogger 往文件里面写日志相关代码 type FileLogger struct { level LogLevel filePath string // 日志文件保存路径 fileName string // 日志文件名 maxFileSize int64 // 最大的文件大小 fileObj *os.File errFileObj *os.File logChan chan *logMsg } type logMsg struct { Level LogLevel msg string funcName string line int fileName string timestamp string // 时间戳 } // NewFileLogger ... func NewFileLogger(levelStr, fp, fn string, maxSize int64) *FileLogger { logLevel, err := parseLogLevel(levelStr) if err != nil { panic(err) } fl := &FileLogger{ level: logLevel, filePath: fp, fileName: fn, maxFileSize: maxSize, logChan: make(chan *logMsg, 50000), // 容量大小五万的日志通道 } err = fl.initFile() // 打开文件,获取文件对象 if err != nil { panic(err) } return fl } func (f *FileLogger) initFile() error { // 创建记录正确的日志文件 fullFileName := filepath.Join(f.filePath, f.fileName) fileObj, err := os.OpenFile(fullFileName+".log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Printf("open log file faild,err:%v", err) return err } // 创建错误的日志文件 errFileObj, err := os.OpenFile(fullFileName+"_err.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Printf("open err log file faild,err:%v", err) return err } f.fileObj = fileObj f.errFileObj = errFileObj // 开启后台goroutine写日志 go f.writeLogBackground() return nil } func (f *FileLogger) enable(logLevel LogLevel) bool { return f.level <= logLevel } func (f *FileLogger) writeLogBackground() { for { if f.checkSize(f.fileObj) { newFile, err := f.splitFile(f.fileObj) if err != nil { return } f.fileObj = newFile } select { case logTmp := <-f.logChan: logInfo := fmt.Sprintf("[%s] [%s] [%s:%s:%d] %s \n", logTmp.timestamp, getLogString(logTmp.Level), logTmp.fileName, logTmp.funcName, logTmp.line, logTmp.msg) fmt.Fprintf(f.fileObj, logInfo) if logTmp.Level >= ERROR { if f.checkSize(f.errFileObj) { newFile, err := f.splitFile(f.errFileObj) if err != nil { return } f.errFileObj = newFile } // 如果记录日志级别大于或等于ERROR,则再记录一份到LogErr的文件中 fmt.Fprintf(f.fileObj, logInfo) } default: // 等五百毫秒 time.Sleep(time.Millisecond * 500) } } } func (f *FileLogger) log(lv LogLevel, format string, args ...interface{}) { if f.enable(lv) { msg := fmt.Sprintf(format, args...) // 合并输出 funcName, fileName, lineNo := getInfo(3) // 三层调用 now := time.Now().Format("2006-01-02 03:04:06") // 日志发送到通道中 logTmp := &logMsg{ Level: lv, msg: msg, funcName: funcName, fileName: fileName, timestamp: now, line: lineNo, } select { case f.logChan <- logTmp: // 信息,写入通道 default: // 如果写满了通道就不写了,丢掉写不进去的日志 } } } // Debug ... func (f *FileLogger) Debug(format string, args ...interface{}) { f.log(DEBUG, format, args...) } // Trace ... func (f *FileLogger) Trace(format string, args ...interface{}) { f.log(TRACE, format, args...) } // Info ... func (f *FileLogger) Info(format string, args ...interface{}) { f.log(INFO, format, args...) } // Warning ... func (f *FileLogger) Warning(format string, args ...interface{}) { f.log(WARNING, format, args...) } // Error ... func (f *FileLogger) Error(format string, args ...interface{}) { f.log(ERROR, format, args...) } // Fatal ... func (f *FileLogger) Fatal(format string, args ...interface{}) { f.log(FATAL, format, args...) } // Close 关闭文件资源 func (f *FileLogger) Close() { f.fileObj.Close() f.errFileObj.Close() } // 获取文件大小,判断是否要进行切割 func (f *FileLogger) checkSize(file *os.File) bool { fileInfo, err := file.Stat() if err != nil { fmt.Printf("get file info failed,err%v\n", err) return false } // 如果当前文件的size大于设定的size,则返回true,否则返回false return fileInfo.Size() >= f.maxFileSize } func (f *FileLogger) splitFile(file *os.File) (*os.File, error) { nowStr := time.Now().Format("20060102150405000") fileInfo, err := file.Stat() if err != nil { fmt.Printf("get file info failed,err:%v\n", err) return nil, err } logName := filepath.Join(f.filePath, fileInfo.Name()) newlogName := fmt.Sprintf("%s.%s.bak", logName, nowStr) // 1. 关闭当前文件 file.Close() // 2. 备份一个 rename os.Rename(logName, newlogName) // 3. 打开一个新的日志文件 fileObj, err := os.OpenFile(logName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Printf("open log file failed, err:%v", err) return nil, err } // 4. 将打开的文件赋值给 fl.FileObj return fileObj, nil }