zoukankan      html  css  js  c++  java
  • 日志库项目

    日志库项目

    需求分析

    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
    ",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
    ", 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
    ",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
    ",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
    ",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 
    ")
    		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
    ",err)
    		return nil,err
    	}
    	return fileObj, nil //返回新的文件句柄
    }
    func (f *FileLogger)checkSize(file *os.File) bool{
    
    	//test
    	fmt.Printf("此时进行比较的文件句柄为%v
    ",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
    ",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
    ", 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
    ", 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
    ", 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
    ",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 
    ")
    		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
    ",err)
    		return nil,err
    	}
    	return fileObj, nil //返回新的文件句柄
    }
    

    该程序还能对指定级别的日志信息单独存储,例如本文可以对ERROR级别以上的日志单独存储再一个文件中。

    所学到的知识

    这个项目是看Qimi老师的视频所练习。

    本项目用到的一些go的基础知识有:

    • 文件的读写:我认为这是这个小项目最核心的部分
    • time包的使用
    • runtime包的使用
    • 模块化的设计思想
    • os包的使用
    • 如何格式化的接受数据
    • string包的使用
  • 相关阅读:
    做前端的一些小工具
    分析几种编程语言对JSON的支持程度
    注册中心eureka
    搭建分布式配置中心
    接口幂等性
    分布式限流
    服务容错解决方案
    微服务架构认知
    gateWay
    JWT鉴权
  • 原文地址:https://www.cnblogs.com/wind-zhou/p/12940595.html
Copyright © 2011-2022 走看看