日志库项目
日志库项目
需求分析
-
支持往不同地方输出(文件、终端、kafka...)
-
日志的级别
- Debug
- Trace
- Info
- warring
- Error
- Fatal
-
日志支持开关控制
例如开发时打印所有日志,但上线是只打印Info级别以上日志
-
日志要有具体信息:时间、行号、文件名、日志级别、日志信息
-
日志要切割
- 按文件大小切割,
源码
项目目录:
main.go
package main
import (
"fmt"
"github.com/logger"
)
var log logger.ConLogger
//测试自己的日志
func main(){
log=logger.NewConsoleLog("info")
log=logger.NewFileLogger("info","./","logtest.log",10*1024*100)
fmt.Printf("%v\n",log)
for {
log.Debug("这是个debug日志")
log.Trace("这是个trace日志")
id:=100
name:="周正"
log.Info("这是个info日志")
log.Warring("这是warring日志")
log.Error("这是个error日志 %d %s",id,name)
log.Fatal("这是个fatal日志")
//time.Sleep(time.Second)
}
}
mylogger.go:
package logger
import (
"errors"
"path"
"runtime"
"strings"
)
type LogLevel uint16
const (
//日志级别。参照time包里的常量设置
UNKNOW LogLevel =iota
TRACE
DEBUG
INFO
WARRING
ERROR
FATAL
)
//构造一个接口,用于同意file和console的结构体
type ConLogger interface {
Debug(format string,a...interface{})
Trace(format string,a...interface{})
Info(format string,a...interface{})
Warring(format string,a...interface{})
Error(format string,a...interface{})
Fatal(format string,a...interface{})
}
//将输入的string型的日志级别,解析从我们设置的 LogLevel型级别,用于后面做比较
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 "warring":
return WARRING,nil
case "error":
return ERROR,nil
case "fatal":
return FATAL,nil
default:
err:=errors.New("无效日志级别")
return UNKNOW ,err
}
}
//得到程序一些运行时的参数
func getINfo(n int)(funcName string,fileName string,lineNo int){
pc,file,lineNo ,ok:=runtime.Caller(n)
if !ok {
return
}
funcName=runtime.FuncForPC(pc).Name()//函数的名称
fileName=path.Base(file) //运行此语句的文件名
funcName=strings.Split(funcName,".")[1] //切割第一个字段
return
}
//相当于反向解析,根据传入的数学形式的级别解析成string型,用于后面的日志信息构造
func getLogString(lv LogLevel)string{
switch lv{
case DEBUG:
return "DEBUG"
case TRACE:
return "TRACE"
case WARRING:
return "WARRING"
case INFO:
return "INFO"
case ERROR:
return "ERROR"
case FATAL:
return "FATAL"
}
return "DEBUG"
}
console.go:
package logger
import (
"fmt"
"time"
)
//终端输出日志
//构造函数
func NewConsoleLog(levelstr string)Logger{
//这里接受的level参数是string,如果进行比较还需转一下,装换成logLevel
levlel,err:=parseLogLevel(levelstr)
if err != nil {
panic(err)
}
return Logger{
Level: levlel,
}
}
//构造结构体,这个仅需要一个参数
type Logger struct {
Level LogLevel
}
//比较当前日志级别和要求的日志级别的大小
func (l Logger)enabled(level LogLevel)bool{
return level>l.Level
}
//日志输出的核心代码,包括判断日志级别,构造日志信息,以及输出等
func (l Logger)log(lv LogLevel,format string,a...interface{}){
if l.enabled(lv) {
msg := fmt.Sprintf(format, a...)
funcName, fileName, lineNo := getINfo(3)
now := time.Now()
fmt.Printf("[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), funcName, fileName, lineNo, msg)
}
}
//下面几个时不同级别日志的输出
func (l Logger)Debug(format string,a...interface{}){
l.log(DEBUG,format ,a...)
}
func (l Logger)Trace(format string,a...interface{}){
l.log(TRACE,format ,a...)
}
func (l Logger)Info(format string,a...interface{}){
l.log(INFO,format ,a...)
}
func (l Logger)Warring(format string,a...interface{}){
l.log(WARRING,format ,a...)
}
func (l Logger)Error(format string,a...interface{}){
l.log(ERROR,format ,a...)
}
func (l Logger)Fatal(format string,a...interface{}){
l.log(FATAL,format ,a...)
}
file.go:
package logger
import (
"fmt"
"os"
"path"
"time"
)
//往文件里写日志
//结构体
type FileLogger struct {
level LogLevel
filePath string //日志文件保存路径
fileName string //文件名
FileLogObj *os.File //用于存储日志的句柄
errFileLogObj *os.File //用于单独存储错误日志的句柄
maxFileSzie int64 //日志文件容量,超过就进行切割
}
//NewFileLogger 创建一个日志实例
func NewFileLogger(lelvelstr,filePath,fileName string ,maxFileSzie int64)*FileLogger{
level,err:=parseLogLevel(lelvelstr)
if err != nil {
panic(err)
}
f1:= &FileLogger{
level: level,
filePath: filePath,
fileName: fileName,
maxFileSzie: maxFileSzie,
}
//调用函数,完成两个句柄的初始化
err=f1.fileObjInit()
return f1
}
//完成FileLogger结构体中两个句柄的初始化
func (f *FileLogger)fileObjInit()(error){
fileLogObjPath:=path.Join(f.filePath,f.fileName)//拼接路径
fileLogObj,err:=os.OpenFile(fileLogObjPath,os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)//打开路径指定文件,获得句柄
if err != nil {
fmt.Printf("打开文件失败:err=%v\n",err)
return err
}
errFileLogObj,err:=os.OpenFile(fileLogObjPath+".err",os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)//打开路径指定文件,获得句柄,存储满足一定条件级别的日志
if err != nil {
fmt.Printf("打开文件失败:err=%v\n",err)
return err
}
f.FileLogObj=fileLogObj
f.errFileLogObj=errFileLogObj
return nil
}
//比较当前日志级别和要求的日志级别的大小
func (f FileLogger)enabled(level LogLevel)bool{
return level>f.level
}
//按大小切割文件
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.back+时间戳
err=os.Rename(logName,newLogName) //重命名
if err != nil {
fmt.Printf("rename failed \n")
return nil, err
}
//3. 打开一个新的日志文件(按原名称)
fileObj,err:=os.OpenFile(logName,os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("打开新文件失败 err:=%v\n",err)
return nil,err
}
return fileObj, nil //返回新的文件句柄
}
func (f *FileLogger)checkSize(file *os.File) bool{
//test
fmt.Printf("此时进行比较的文件句柄为%v\n",file)
//fileInfo,err:=f.fileLogObj.Stat()
fileInfo,err:=file.Stat()
if err != nil {
//test
fmt.Println("问题出现在这 222")
fmt.Printf("get file info failed err=%v\n",err)
panic(err)
}//如果当前文件大小大于设定的最大值,返回true
return fileInfo.Size()>=f.maxFileSzie
}
//核心处理单元
func (f *FileLogger)log(lv LogLevel,format string,a...interface{}){
if f.enabled(lv) {
msg := fmt.Sprintf(format, a...)
if f.checkSize(f.FileLogObj) {//判断要切割日志
newFile,err:=f.splitFile(f.FileLogObj)
if err != nil {
return
}
f.FileLogObj=newFile
}
now := time.Now()
fmt.Fprintf(f.FileLogObj,"[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), funcName, fileName, lineNo, msg)//这里用的是Fprintf,因为可以指定输出位置
if lv>= ERROR{//超过ERROR级别的日志单独存储
if f.checkSize(f.errFileLogObj){//判断要切割日志
newFile,err:=f.splitFile(f.errFileLogObj)
if err != nil {
return
}
f.errFileLogObj=newFile
}
fmt.Fprintf(f.errFileLogObj,"[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), funcName, fileName, lineNo, msg)
}
}
}
func (f *FileLogger)Debug(format string,a...interface{}){
f.log(DEBUG,format ,a...)
}
func (f *FileLogger)Trace(format string,a...interface{}){
f.log(TRACE,format ,a...)
}
func (f *FileLogger)Info(format string,a...interface{}){
f.log(INFO,format ,a...)
}
func (f *FileLogger)Warring(format string,a...interface{}){
f.log(WARRING,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...)
}
代码分析
各个包的作用
代码一共两个模块,主测试模块的日志库实现模块。
日志库实现模块又分为两个部分:往终端打印日志和往文件打印日志。分别对应logger包中的console.go和file.go,
mylogger.go主要存放包中一些共用的函数、结构体变量等。
代码逻辑简介
代码采用面向对象的设计方法。(这是最需要学习的)
就是构造一个结构体,里面设置若干参数字段,然后为其设计相应函数来实现特定功能。
首先在main函数中调用构造函数来创建一个相应的结构体
然后主函数中用一个for循环来调用各个级别日志的输出执行函数。
在调用执行函数时,传入某个级别的日志名称,然后首先在函数中进行级别的比较,若符合级别要求,则继续执行。
这里构造了一个函数进行级别比较,如果满足则返回一个bool类型的true。
接下来便根据传入的内容构造日志信息,代码如下:
msg := fmt.Sprintf(format, a...)//构造信息
funcName, fileName, lineNo := getINfo(3)//得到行号,文件名,函数名
now := time.Now() //得到时间
fmt.Fprintf(f.FileLogObj,"[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), funcName, fileName, lineNo, msg)//项指定文件输出拼接的日志信息
解释:这里构造的信息要满足前面我们的需求,有时间,行号,日志级别等。
第一行的构造信息有点意思,他可以实现类似printf函数的格式化输入,如下图
这里貌似很高级,因此要实现格式化的输入需要底层的一些东西,例如反射。但这里我们并不需要自己写如何实现,因为我们只是套了个壳子,因为在我们在函数里立马借用了 fmt.Sprintf(format, a...)
替我们实现了格式化的识别。
实现时间的操作使用了time包。
实现行号,函数名等操作使用了runtime包。runtime负责记录一些程序运行时的信息。
getINfo()函数实现如下:
在其中要判断日志是否需要切割,这里时按大小切割。
因此大概需要两步:
-
判断当前文件大小是否达到要求。
-
若达到要求则进行切割
文件切割的逻辑如下:
- 关闭文件(调用close()的方法)
- 备份当前文件(就是将当前文件重命名成一个备份文件)
- 根据原文件名再创建和打开一个新的文件
- 返回当前新文件的句柄(为后面写文件提供一个新路径)
//按大小切割文件
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.back+时间戳
err=os.Rename(logName,newLogName) //重命名
if err != nil {
fmt.Printf("rename failed \n")
return nil, err
}
//3. 打开一个新的日志文件(按原名称)
fileObj,err:=os.OpenFile(logName,os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("打开新文件失败 err:=%v\n",err)
return nil,err
}
return fileObj, nil //返回新的文件句柄
}
该程序还能对指定级别的日志信息单独存储,例如本文可以对ERROR级别以上的日志单独存储再一个文件中。
所学到的知识
这个项目是看Qimi老师的视频所练习。
本项目用到的一些go的基础知识有:
- 文件的读写:我认为这是这个小项目最核心的部分
- time包的使用
- runtime包的使用
- 模块化的设计思想
- os包的使用
- 如何格式化的接受数据
- string包的使用