日志库项目

日志库项目

需求分析

  1. 支持往不同地方输出(文件、终端、kafka...)

  2. 日志的级别

    • Debug
    • Trace
    • Info
    • warring
    • Error
    • Fatal
  3. 日志支持开关控制

    例如开发时打印所有日志,但上线是只打印Info级别以上日志

  4. 日志要有具体信息:时间、行号、文件名、日志级别、日志信息

  5. 日志要切割

    • 按文件大小切割,

源码

项目目录:

mark

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函数中调用构造函数来创建一个相应的结构体

mark

然后主函数中用一个for循环来调用各个级别日志的输出执行函数。

在调用执行函数时,传入某个级别的日志名称,然后首先在函数中进行级别的比较,若符合级别要求,则继续执行。

这里构造了一个函数进行级别比较,如果满足则返回一个bool类型的true。

mark

接下来便根据传入的内容构造日志信息,代码如下:

	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函数的格式化输入,如下图

mark

这里貌似很高级,因此要实现格式化的输入需要底层的一些东西,例如反射。但这里我们并不需要自己写如何实现,因为我们只是套了个壳子,因为在我们在函数里立马借用了 fmt.Sprintf(format, a...)替我们实现了格式化的识别。

实现时间的操作使用了time包。

实现行号,函数名等操作使用了runtime包。runtime负责记录一些程序运行时的信息。

getINfo()函数实现如下:

mark

在其中要判断日志是否需要切割,这里时按大小切割。

mark

因此大概需要两步:

  1. 判断当前文件大小是否达到要求。

  2. 若达到要求则进行切割

    文件切割的逻辑如下:

    1. 关闭文件(调用close()的方法)
    2. 备份当前文件(就是将当前文件重命名成一个备份文件)
    3. 根据原文件名再创建和打开一个新的文件
    4. 返回当前新文件的句柄(为后面写文件提供一个新路径)
//按大小切割文件
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包的使用
posted @ 2020-05-23 00:08  wind-zhou  Views(174)  Comments(0Edit  收藏  举报