GO语言-文件版日志系统

一、背景

春节期间在B站学习了GO语言的函数,方法,指针,切片,结构体,文件,Map,接口,time等基础知识,需要一个项目进行综合性练手来更好的理解这些基础,所以编写了一个日志系统,供正在学习GO语言的同学参考。

二、系统原理解析

2.1 目录结构

  • main包下的main.go

 

  • mylogger包下的 console.go,file.go,mylogger.go

2.2 main包下的go文件

2.2.1 main.go

  • 通过调用mylogger包下的不同的构造方法【NewConsoleLogger()或NewFileLogger()】,会返回不同的对象
  • 返回的ConsoleLogger或*FileLogger类型的对象可以赋值给mylogger包下的LoggerInterface接口,因为这两类对象全部实现了接口中定义的方法【Debug,Info,Warn...】
  • 通过写一个for{}循环模拟日志输出,输出到console还是文件根据调用的构造方法返回不同类型的对象决定
package main

import (
	"time"
	mylogger "github.com/studygo/day01/07_mylogger"
)

var log mylogger.LoggerInterface //在main包里面定义log变量,可以在main包的其他文件中使用

func main() {
	log := mylogger.NewConsoleLogger("Info")
	// log = mylogger.NewFileLogger("info", "./", "accessTest.log", 10*1024*1024)
	for {
		log.Debug("这是一条Debug日志")
		log.Info("这是一条Info日志")
		log.Warn("这是一条Warn日志")
		id := 10010
		name := "kelly"
		log.Error("这是一条Error日志,id:%d,name:%s", id, name)
		log.Fatal("这是一条Fetal日志")
		// fmt.Printf("id:%d,name:%s", id, name)
		log.Fetal("这是一条Fetal日志")
		time.Sleep(2 * time.Second)
	}
}

2.3 mylogger包下的go文件

2.2.1 日志系统的公共变量和方法

1. mylogger.go

  • 定义了一个自定义类型为LogLevel,不同于类型别名的是自定义类型在编译完成之后这种类型依然存在。详细参考:类型别名和自定义类型区别
  • 定义DEBUG,INFO,WARN,ERROR,FATAL等LogLevel类型的常量
  • 定义LoggerInterface接口,这个接口下定义了五大抽象方法,只要实现了这些方法的对象就可以称为LoggerInterface类型。详细参考:GO语言接口
  • 定义将字符串解析为LogLevel类型的方法和将LogLevel类型方法转换成string类型的方法
package mylogger

/*
1.支持往不同的地方输出日志
2.日志分级别
	1.Debug
	2.Trace
	3.Info
	4.Warning
	5.Error
	6.Fatal
3.日志需要开关控制,比如说开发的时候什么级别都能输出,但是上线之后只能有INFO级别往下的日志才可以输出
4.日志要有时间,行号(runtime.Caller()),文件名,日志级别,日志信息
5.日志文件要切割
	5.1 按文件大小切割
	5.2 按文件日期区分
*/

//往终端写日志相关的内容

import (
	"errors"
	"strings"
)

//LogLevel 等级
type LogLevel uint16

const (
	// UNKNOW switch中的默认参数
	UNKNOW LogLevel = iota
	//DEBUG ...
	DEBUG
	//TRACE ...
	TRACE
	//INFO ...
	INFO
	//WARN ...
	WARN
	//ERROR ...
	ERROR
	//FATAL ...
	FATAL
)

//LoggerInterface 日志接口,可以用来接收console和file的返回值
type LoggerInterface interface {
	Debug(msg string, arg ...interface{})
	Info(msg string, arg ...interface{})
	Warn(msg string, arg ...interface{})
	Error(msg string, arg ...interface{})
	Fatal(msg string, arg ...interface{})
}

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 "warn":
		return WARN, nil
	case "error":
		return ERROR, nil
	case "fatal":
		return FATAL, nil
	default:
		err := errors.New("无效的日志级别")
		return UNKNOW, err
	}
}

func getLogString(lv LogLevel) string {

	switch lv {
	case DEBUG:
		return "DEBUG"
	case TRACE:
		return "TRACE"
	case INFO:
		return "INFO"
	case WARN:
		return "WARING"
	case ERROR:
		return "ERROR"
	case FATAL:
		return "FATAL"
	default:
		return "UNKNOW"
	}
}

2.2.2 控制台输出日志信息

1. file.go

  • 定义了NewFileLogger构造函数,返回的是一个FileLogger类型的结构体指针,通过parseLogLevel()方法把字符串类型转换成LogLevel类型,构造函数还需要做的是根据传入的文件路径和文件名打开文件,为了方便开发查看ERROR以上级别的日志,还需要把这些日志单独记录到一个文件中,最后把fileObj,errObj赋值给指针变量f的fileObj,errorObj字段。
  • 使用file.go文件下的Debug,Info,Warn,Error等方法,这些方法会调用enable进行判断是否高于指定日志级别【本文中在构造函数中传入的日志级别为Info,filepath为当前目录 "./",filename为 "accessTest.log",文件切割临界为 "10*1024*1024"(10KB)】,如果返回为true则调用具体的log方法在文件中记录日志信息,反之则不会打印打文件
  • log方法具体实现就是先判断日志级别是否大于等于传入的级别,返回true则继续调用checkSize方法判断文件大小是否大于传入的临界值,返回true则继续调用split方法对日志进行切割,详细原理见下方代码
  • 额外的,如果要记录的日志大于等于ERROR级别,我还要在err日志中再记录一遍
package mylogger

import (
	"fmt"
	"os"
	"path"
	"time"
)

//FileLogger 往文件里面写日志相关代码  文件日志结构体
type FileLogger struct {
	Level       LogLevel
	filePath    string
	fileName    string
	fileObj     *os.File
	errorObj    *os.File
	maxFileSize int64
}

//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 {
	fullName := path.Join(f.filePath, f.fileName)
	fileObj, err := os.OpenFile(fullName, 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
	}

	errObj, err := os.OpenFile(fullName+".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.errorObj = errObj
	return nil
}

//判断是否需要记录该日志
func (f *FileLogger) enable(logLevel LogLevel) bool {
	return logLevel >= f.Level
}

//根据大小判断文件是否需要切割
func (f *FileLogger) checkSize(file *os.File) bool {
	fileInfo, err := file.Stat()
	if err != nil {
		panic(err)
	}
	return fileInfo.Size() >= f.maxFileSize //如果传入的文件大小大于等于文件日志的最大值,就应该返回true
}

//切割文件
func (f *FileLogger) splitFile(file *os.File) (*os.File, error) {
	// 需要切割日志文件
	nowStr := time.Now().Format("20160102150405000")
	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
	os.Rename(logName, newLogName)
	//3.打开一个新的日志文件
	fileObj, err := os.OpenFile(logName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Printf("file open failed:err:%v", err)
		return nil, err
	}
	//4.将打开的日志文件对象赋值给 f.fileObj
	return fileObj, nil
}

//记录日志的方法
func (f *FileLogger) log(lv LogLevel, msg string, arg ...interface{}) {
	if f.enable(lv) {
		// fmt.Println(msg)
		fullMsg := fmt.Sprintf(msg, arg...)
		// fmt.Println(fullMsg)
		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), funcName, fileName, lineNo, fullMsg)
		if lv >= ERROR { //如果要记录的日志大于等于ERROR级别,我还要在err日志中再记录一遍
			if f.checkSize(f.fileObj) {
				newFile, err := f.splitFile(f.fileObj)
				if err != nil {
					return
				}
				f.fileObj = newFile
			}
			fmt.Fprintf(f.errorObj, "[%s] [%s] [%s:%s:%d] %s\n", now.Format("2006-01-02 15:04:05"), getLogString(lv), funcName, fileName, lineNo, fullMsg)
		}
	}
}

//Debug 方法
func (f *FileLogger) Debug(msg string, arg ...interface{}) {

	f.log(DEBUG, msg, arg...)
}

//Info 方法
func (f *FileLogger) Info(msg string, arg ...interface{}) {
	f.log(INFO, msg, arg...)

}

//Warn 方法
func (f *FileLogger) Warn(msg string, arg ...interface{}) {
	f.log(INFO, msg, arg...)
}

//Error 方法
func (f *FileLogger) Error(msg string, arg ...interface{}) {
	f.log(ERROR, msg, arg...)
}

//Fatal 方法
func (f *FileLogger) Fatal(msg string, arg ...interface{}) {
	f.log(FATAL, msg, arg...)
}

2.日志输出到文件结果验证

  • 在main.go的同级目录下新增了accessTest.log,accessTest.log.bak202002011437...
  • 通过查看文件大小发现文件大小为10KB,说明已经按照我们预期的文件大小阈值进行切割了
  • accessTest.log中包含大于等于指定类型的日志信息,accessTest.log.err中包含大于等于ERROR类型的日志信息

发布了171 篇原创文章 · 获赞 40 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_36441027/article/details/104132182