自定义日志库实现
需求分析
- 1.支持往不同的地方输出日志
- 2.日志分级别
- (1) Debug
- (2) Trace
- (3) Info
- (4) Warning
- (5) Error
- (6) Fatal
- 3.日志要支持开关控制,比如说开发的时候什么级别的都能输出,但是上线之后只有Info级别往下的才能输出
- 4.完整的日志记录要包含有时间、行号、文件名、日志级别、日志信息
- 5.日志文件要切割
-
(1) 按文件大小切割
每次记录日志之前判断一下当前写的这个文件的文件大小
-
(1) 按文件大小切割
- (2) 按日期切割
补充知识点runtime.Caller()
这个函数主要是为了查看函数调用栈的问题
pc,file,line, ok := runtime.Caller(n)
返回4个值:
n是要提升的堆栈帧数,0-当前函数,1-上一层函数: 就是说为0时,是runtime.Caller执行的位置,line会显示runtime的行号,为1时,会显示执行runtime这个函数上外包的函数的行号,以此类推。
pc是uintptr这个返回的是函数指针, 通过runtime.FuncForPC(pc).Name() 可以获取该函数的函数名
file是函数所在文件名目录
line所在行号
ok 是否可以获取到信息
package main
import (
"fmt"
"path"
"runtime"
)
func main() {
pc, file, line, ok := runtime.Caller(0)
if !ok {
fmt.Printf("runtime.Caller() failed\n")
return
}
funcName := runtime.FuncForPC(pc).Name()
fmt.Println(funcName)
fmt.Println(file)
fmt.Println(path.Base(file))
fmt.Println(line)
}
简单实现
文件目录
----mylogger
|_____console.go
|_____file.go
|_____mylogger.go
----mylogger_test
|_____main.go
通用日志mylogger.go
package mylogger
import (
"errors"
"fmt"
"path"
"runtime"
"strings"
)
// 自定义一个日志库
type LogLevel uint16
// Logger 接口
type Logger interface {
Debug(format string, a ...interface{})
Info(format string, a ...interface{})
Warning(format string, a ...interface{})
Error(format string, a ...interface{})
Fatal(format string, a ...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"
}
return "DEBUG"
}
// 获取执行文件名、函数、行号
func getInfo(skip int) (funcName, fileName string, lineNo int) {
pc, file, lineNo, ok := runtime.Caller(skip)
if !ok {
fmt.Printf("runtime.Caller() failed\n")
return
}
funcName = runtime.FuncForPC(pc).Name()
fileName = path.Base(file)
funcName = strings.Split(funcName, ".")[1]
return
}
控制台打印日志console.go
package mylogger
import (
"fmt"
"time"
)
// 往终端写日志相关内容
// Logger 日志结构体
type ConsoleLogger struct {
Level LogLevel
}
// NewLog 构造函数
func NewConsoleLog(LeveStr string) ConsoleLogger {
level, err := parseLogLevel(LeveStr)
if err != nil {
panic(err)
}
return ConsoleLogger{
Level: level,
}
}
// 根据日志级别控制日志输出
func (c ConsoleLogger) enable(LogLevel LogLevel) bool {
return LogLevel >= c.Level
}
// 日志输出格式
func (c ConsoleLogger) log(lv LogLevel, format string, a ...interface{}) {
if c.enable(lv) {
msg := fmt.Sprintf(format, a...)
now := time.Now()
funcName, fileName, lineNo := getInfo(3)
fmt.Printf("[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), fileName, funcName, lineNo, msg)
}
}
func (c ConsoleLogger) Debug(format string, a ...interface{}) {
c.log(DEBUG, format, a...)
}
func (c ConsoleLogger) Info(format string, a ...interface{}) {
c.log(INFO, format, a...)
}
func (c ConsoleLogger) Warning(format string, a ...interface{}) {
c.log(WARNING, format, a...)
}
func (c ConsoleLogger) Error(format string, a ...interface{}) {
c.log(ERROR, format, a...)
}
func (c ConsoleLogger) Fatal(format string, a ...interface{}) {
c.log(FATAL, format, a...)
}
生成日志文件file.go
package mylogger
import (
"fmt"
"os"
"path"
"time"
)
// 往文件里面写日志相关代码
type FileLogger struct {
Level LogLevel
filePath string // 日志文件保存的路径
fileName string // 日志文件保存的文件名
fileObj *os.File
errFileObj *os.File
maxFileSize int64
}
//
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) checkSize(file *os.File) bool {
fileInfo, err := file.Stat()
if err != nil {
fmt.Printf("get file info failed, err:%v\n", err)
return false
}
// 如果当前文件大小 大于等于 日志文件的最大值 就应该返回true
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 := path.Join(f.filePath, fileInfo.Name()) // 拿到当前日志文件完整路径
newLogName := fmt.Sprintf("%s.bak%s", logName, nowStr) // 拼接一个日志文件备份的名字
// 1. 关闭当前的日志文件
file.Close()
// 2. 备份一下 rename xx.log ===> xx.log.bak20220112220435000
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 new log file failed, err:%v\n", err)
return nil, err
}
// 4. 将打开的新日志文件对象赋值给 f.fileObj
return fileObj, nil
}
// 初始化日志文件
func (f *FileLogger) initFile() error {
fullFileName := path.Join(f.filePath, f.fileName)
fileObj, err := os.OpenFile(fullFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("open log file failed, err:%v\n", err)
return err
}
errFileObj, err := os.OpenFile(fullFileName+".err", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("open err log file failed, err:%v\n", err)
return err
}
// 日志文件都已经打开了
f.fileObj = fileObj
f.errFileObj = errFileObj
return nil
}
// 根据日志级别控制日志输出,判断是否记录该日志
func (f *FileLogger) enable(LogLevel LogLevel) bool {
return LogLevel >= f.Level
}
// 日志输出格式
func (f *FileLogger) log(lv LogLevel, format string, a ...interface{}) {
if f.enable(lv) {
msg := fmt.Sprintf(format, a...)
now := time.Now()
funcName, fileName, lineNo := getInfo(3)
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.Format("2006-01-02 15:04:05"), getLogString(lv), 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级别,在err日志文件中再记录一遍
fmt.Fprintf(f.errFileObj, "[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), fileName, funcName, lineNo, msg)
}
}
}
func (f *FileLogger) Debug(format string, a ...interface{}) {
f.log(DEBUG, format, a...)
}
func (f *FileLogger) Info(format string, a ...interface{}) {
f.log(INFO, format, a...)
}
func (f *FileLogger) Warning(format string, a ...interface{}) {
f.log(WARNING, format, a...)
}
func (f *FileLogger) Error(format string, a ...interface{}) {
f.log(ERROR, format, a...)
}
func (f *FileLogger) Fatal(format string, a ...interface{}) {
f.log(FATAL, format, a...)
}
func (f *FileLogger) Close() {
f.fileObj.Close()
f.errFileObj.Close()
}
主函数main.go
package main
import (
// "time"
"code.mhg.com/studygo/mylogger"
)
var log mylogger.Logger // 声明一个全局接口
// 测试我们自己写的日志库
func main() {
// 控制台输出
// log = mylogger.NewConsoleLog("Debug")
// 文件输出
log = mylogger.NewFileLogger("Debug", "./", "mi.log", 1*1024*1024)
for {
id := 10010
name := "理想"
log.Debug("这是一条Debug日志,id:%d,name:%s", id, name)
log.Info("这是一条Info日志")
log.Warning("这是一条Warning日志")
log.Error("这是一条Error日志")
log.Fatal("这是一条Fatal日志")
// time.Sleep(time.Second)
}
}