go goroutine + chan 异步写日志

go 写日志

使用文件目录
/zydhlog
    log.go
    console.go
    log_to_file.go
    #日志文件 - 初始时不可缺
    error.txt
    fatal.txt
    normal.txt
    warning.txt

/zydhfile
    file.go
    

  log.go  自己使用ini文件映射配置

package zydhlog

import (
	"fmt"
	"path"
	"runtime"
	"strconv"
	"strings"
	"time"
)

/**
	日志库
	接口,日志可以输出到终端,也可以输出到文件,输出到kafka

	需求分析
		1.可以不同地方写入
		2.日志分级别
				debug
				trace
				info
				warning
				error
				fatal
		3.日志要支持开关控制,比如开发的时候什么级别都能输出,但是上线后只有info级别的往下的才能输出
		4.日志内容,要有时间,行号,文件名,日志级别,日志信息
		5.日志文件要切割

*/



//日志等级
const(
	ERR_LEVEL_NO 		int		= iota
	ERR_LEVEL_DEBUG
	ERR_LEVEL_TRACE
	ERR_LEVEL_INFO
	ERR_LEVEL_WARNING
	ERR_LEVEL_ERROR
	ERR_LEVEL_FATAL
)

//日志存放路径
const(
	LOG_FILE_PATH_NORMAL 		string = "./zydhlog/normal.txt"		//普通日志路径
	LOG_FILE_PATH_WARNING 		string = "./zydhlog/warning.txt"	//警告日志路径
	LOG_FILE_PATH_ERROR 		string = "./zydhlog/error.txt"		//错误日志路径
	LOG_FILE_PATH_FATAL  		string = "./zydhlog/fatal.txt"		//500服务器内部致命错误日志路径
)

//调试等级,关闭打开 0 关闭,不写入 1 打开
const  IS_OPEN_OR_CLOSE_LOG = 1

//打印位置 0 终端,1 文件 2 通道->文件
const LOG_TO_SITE = 2

//日志通道能放的最大队列数
const LOG_CHAN_MAX_CAP = 1000

//分隔文件的方式 -1 不分割 0 按照文件大小切割 1 按照时间日期切割
const CUT_FILE_TYPE = 0



//LogChan 日志通道
var LogChan chan *LogToFileByGoChan


//初始化
func init(){
	//当开启日志 且 是使用go-chan方法时,初始化通道
	if IS_OPEN_OR_CLOSE_LOG == 1 && LOG_TO_SITE == 2 {
		LogChan = make(chan *LogToFileByGoChan,LOG_CHAN_MAX_CAP) //初始化通道
		runtime.GOMAXPROCS(4)
		//开启通道处理
		for i:=0; i<=3;i++{
			go getChanLogToFile(LogChan)
		}
	}
}


/*
	func  组装日志的内容
	param 	errLevel int 		错误级别
	param 	layer int 		time.Caller()的层
	param 	errInfo 	string 		错误信息
	return string
*/
func LogContent(errLevel,layer int ,errInfo string,arg ...any)string{
	timeString := time.Now().Format("2006-01-02 15:04:05.000")
	txtMap := map[int]string{
		ERR_LEVEL_NO		:	"[normal]",
		ERR_LEVEL_DEBUG		:	"[debug]",
		ERR_LEVEL_TRACE		: 	"[trace]",
		ERR_LEVEL_INFO		: 	"[info]",
		ERR_LEVEL_WARNING	: 	"[warning]",
		ERR_LEVEL_ERROR		: 	"[error]",
		ERR_LEVEL_FATAL		: 	"[fatal]",
	}

															//	fmt.Sprintf("%v",error)  //把错误信息转换成string
	return timeString + "  " + getLayerCode(layer) + "\n" + txtMap[errLevel] + fmt.Sprintf("%v",errInfo) + " | " + fmt.Sprintf("%v",arg) + "\n"
}



/*
	func  获取锚点代码的方法锚点位置 包名/文件名.go/ -> 方法名()  | 行号
	param 	layer int 		层,调用的层级 详细看 study/runtime.go --> runtimeCallerStudy()
	return anchorPosition string  调用此方法的锚点位置

	analysis:
		//pc:调用函数的一些信息	file :文件名,当前执行的文件名xxx.go/谁调用了当前函数	line : 调用的多少行	ok :如果能取到,ok == true
		//fmt.Println(pc,file,line) //8726872  D:/G-workSpace/golang/src/go-restudy/study/test.go 71
		//fmt.Println( path.Base(file),runtime.FuncForPC(pc).Name(),line  ) //test.go go-restudy/study.zydhLog 71
		//fmt.Println( pkgNameAndFuncName[0] + "/" + path.Base(file) + " -> " + pkgNameAndFuncName[1] + "()" + " | " + strconv.Itoa(line) + "行" )
		//go-restudy/study/test.go -> zydhLog() | 71行
*/
func getLayerCode(layer int)string{
	//传递参数,可以拿到当前执行程序执行隔了多少层
	pc,file,line,ok := runtime.Caller(layer)
	if !ok {
		return "use runtime.caller() failed," +  "no find layer file name," +"no find layer func name"
	}

	//获得报名和文件名
	pkgNameAndFuncName := strings.Split(runtime.FuncForPC(pc).Name(),".")

	return pkgNameAndFuncName[0] + "/" + path.Base(file) + " -> " + pkgNameAndFuncName[1] + "()" + " | " + strconv.Itoa(line) + "行"
	//go-restudy/study/test.go -> zydhLog() | 71行
}

/*
	func : 把日志写入到场景[通道]
	param : errLevel int 错误级别
	param : logFilePath string 日志存放位置
	param : logMsg string 日志信息
	param : arg ...any
*/
func logWriteToScene(errLevel int,logFilePath,logMsg string,arg ...any){
	//是否开启日志
	if IS_OPEN_OR_CLOSE_LOG > 0{
		logmsg := LogContent(errLevel,4,logMsg,arg)
		//开启后,日志写到哪的场景
		switch LOG_TO_SITE{
		case 1: 	//文件 no goroutine
			writeLogToFile(errLevel,logFilePath,logmsg)
		case 2: 	//发送到通道 ToChan
			select{
			case LogChan <- NewLogToFileByGoChan(errLevel,logFilePath,logmsg):	//当通道没满时,走这
			default:	//当通道满了,走这,丢弃日志,保证不出现阻塞
			}
		default: 	//终端
			outputToTerminal(logmsg)
		}
	}
}



//实现方法 - 不使用接口,接口每次都要先实例化一个对应的结构体,错误

func Trace(logInfo string,arg ...any){
	logWriteToScene(ERR_LEVEL_TRACE,LOG_FILE_PATH_NORMAL,logInfo,arg)
}

func Debug(logInfo string,arg ...any){
	logWriteToScene(ERR_LEVEL_DEBUG,LOG_FILE_PATH_NORMAL,logInfo,arg)
}

func Warning(logInfo string,arg ...any){
	logWriteToScene(ERR_LEVEL_WARNING,LOG_FILE_PATH_WARNING,logInfo,arg)
}

func Info(logInfo string,arg ...interface{}){
	logWriteToScene(ERR_LEVEL_INFO,LOG_FILE_PATH_NORMAL,logInfo,arg)
}

func Error(logInfo string,arg ...any){
	logWriteToScene(ERR_LEVEL_ERROR,LOG_FILE_PATH_ERROR,logInfo,arg)
}

func Fatal(logInfo string,arg ...any){
	logWriteToScene(ERR_LEVEL_FATAL,LOG_FILE_PATH_FATAL,logInfo,arg)
}

  

console.go

package zydhlog

import (
	"fmt"
	"os"
)

/*
	.go : 把日志打印到终端
	author : 朝游东海
*/


/*
	func : 往终端输出
	param : errLevel int 错误级别
	param : logInfo string 日志信息
	param : arg ...any
	analysis:
		//fmt.Println() //这个方法是往终端打印
		//fmt.Fprintln("xx.file",logInfo) //这个方法是往文件写入 需要 OpenFile()文件
*/
func outputToTerminal(logMsg string){
	//往终端写入
	fmt.Fprint(os.Stdout,logMsg)
}

  log_to_file.go

package zydhlog

import (
	"fmt"
	"go-restudy/zydhfile"
	"os"
	"time"
)

/*
	.go : 把日志写入文件 - no goroutine
			5.24 update:通过 goroutine + channel 写日志
	author : 朝游东海
*/



//LogToFileByGoChan 写入通道的日志格式
type LogToFileByGoChan struct {
	leveL int //错误级别
	logFilePath string //日志存放位置
	logMsg string //错误的信息
}

//NewLogToFileByGoChan 初始化
func NewLogToFileByGoChan(level int,logFilePath,logMsg string) *LogToFileByGoChan {
	return &LogToFileByGoChan{
		leveL : level,
		logFilePath : logFilePath,
		logMsg : logMsg,
	}
}


/*
	func : 启动时,开启goroutine处理通道数据
	param : ch 只读通道  *LogToFileByGoChan类型
*/
func getChanLogToFile(ch <- chan *LogToFileByGoChan){
	for{
		select {
		//当通道有值时
		case logData := <- ch:
			writeLogToFile(logData.leveL,logData.logFilePath,logData.logMsg)
		default:
			time.Sleep(time.Millisecond * 500)  //当通道无值时,交出cpu控制权
		}
	}
}


/*
	func  写[通道]日志到文件
	param 	errLevel int 		错误级别
	param 	logFilePath 	string 		日志文件存放路径
	param 	errInfo 	string 		错误信息
*/
func writeLogToFile(errLevel int,logFilePath ,logMsg string,arg ...any){
	if errLevel >= 0 {

		// 是否切割文件 获取日志文件地址
		filePath := fileIsOpenCut(logFilePath)

		//打开文件 //判断文件是否存在
		f,err := zydhfile.CreatFile(filePath,os.O_WRONLY|os.O_CREATE|os.O_APPEND,0644);if err != nil{
			fmt.Printf("creat file false,err %v \n",err)
			return
		}
		defer f.Close()

		//直接写入字符串数据
		_,err =f.WriteString(logMsg);if err != nil{
			fmt.Println("log to file fail,err:",err)
		}
	}
}


/*
	是否开启文件切割
*/
func fileIsOpenCut(logFilePath string)(filePath string){
	if CUT_FILE_TYPE >= 0{
		//切割文件
		switch CUT_FILE_TYPE  {
		case 0: //按照文件大小切割
			filePath = zydhfile.FileCutBySize(logFilePath)
		case 1:  //按照时间切割
			filePath = zydhfile.FileCutByTime(logFilePath)
		default:
			filePath = zydhfile.FileCutByTime(logFilePath)
		}
	}else{
		filePath = logFilePath
	}
	return filePath
}

  /zydhfile

    file.go

package zydhfile

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
	"time"
)

/*
	.go : 自定义的一些文件操作
	author 朝游东海
*/



var DefaultPerm = os.ModeDir|os.ModePerm

//按日期切割文件的名称时间格式
const CUT_FILE_BY_DATE_TIME_FORMAT = "200601021504"
//按文件大小切割文件的名称时间格式
const CUT_FILE_BY_SIZE_TIME_FORMAT = "20060102150405"

//文件超过多大时,切割文件
const LOG_FILE_MAX_SIZE = 1 *1024 // 1 * 1024 * 1024  //1m


//------------------------创建文件------------------------//

/*
	func  	创建目录
	author  zydh[朝游东海]
	param 	dirPath string 		文件路径
	param 	perm 	uint32 		文件权限 0644
	return	bool	返回信息
*/
func MkdirFile(dirPath string,perm os.FileMode ) (bool bool,errInfo error,){
	if perm == 0{
		perm = DefaultPerm
	}
	err := os.Mkdir(dirPath,perm)
	if err !=nil {
		return false,err
	}
	return true,errInfo
}


/*
	func  创建完整目录路径,即中间目录不存在的话也一起创建
	author  zydh[朝游东海]
	param 	dirPath string 		文件路径
	param 	perm 	uint32 		文件权限 0644
	return	bool	返回信息
*/

func MkdirAll(dirPath string,perm os.FileMode) (bool bool,errInfo error,){
	if perm == 0{
		perm = DefaultPerm
	}
	err := os.MkdirAll(dirPath,perm)
	if err !=nil {
		return false,err
	}
	return true,errInfo
}


//创建文件
func CreatFile(path string,flag int,perm os.FileMode)(*os.File,error){
	if flag == 0{
		flag = os.O_CREATE|os.O_TRUNC|os.O_WRONLY
	}
	if perm == 0{
		flag = 0666
	}
	file, err := os.OpenFile(path, flag, perm)
	if err != nil {
		return file, err
	}
	return file,nil
}



//------------------------读文件------------------------//

// GetFileConfigByPath  只读 - 获取文件信息 // 文件名称 //文件大小
func GetFileConfigByPath(path string)(fileName string, fileSize int){
	//f,_ := os.Open(path)
	//fmt.Println(f.Name()) //./zydhlog/warning.txt
	//fmt.Println(f.Stat())
	fo,err := os.Stat(path)
	if err != nil{
		//log.Error("open file fail ,file path:" + path,err)
		return "",0
	}

	return fo.Name(),int(fo.Size())
	//warning.txt //537  //单位,字节
}

// GetFileNameByPath 获取文件的大小 //单位,字节
func GetFileNameByPath(path string) string {
	fo,err := os.Stat(path)
	if err != nil{
		fmt.Println("open file fail ,file path:" + path,err)
		//log.Error("open file fail ,file path:" + path,err)
		return ""
	}
	return fo.Name()
}



//GetFileSizeByPath获取文件的大小 单位,字节
func GetFileSizeByPath(path string) int {
	fo,err := os.Stat(path)
	if err != nil{
		//log.Error("open file fail ,file path:" + path,err)
		return 0
	}
	return int(fo.Size())
}


//------------------------bufio读取文件-----------------------------//

//bufio读取文件  bufio是在file的基础上封装了一层API,支持更多的功能。
//zydh:可自定义读行,分段读
//BufioReadFile 缓冲区读取文件
func BufioReadFile(path string,splitChar byte ){
	if !FileOrDirIsExist(path){
		fmt.Println("path is not exist")
		return
	}

	if splitChar == 0{
		splitChar = '\n'
	}

	file, err := os.Open(path)
	if err != nil {
		fmt.Println("open file failed, err:", err)
		return
	}
	defer file.Close()
	reader := bufio.NewReader(file) //创建读对象 bufio.NewReader()(带缓冲区的方式打开,适合打开较大的文件)
	for {
		line, err := reader.ReadString(splitChar) //注意是字符,单引号  //\n 一行一行的读取 reader.ReadString()(读取文件)
		if err == io.EOF { //io.EOF表示读到文件末尾
			if len(line) != 0 {
				fmt.Println(line)
			}
			fmt.Println("文件读完了")
			break
		}
		if err != nil {
			fmt.Println("read file failed, err:", err)
			return
		}
		fmt.Print(line)  //这里不用fmt.Println()
	}
}




//------------------------写文件------------------------//

func WriteFile(txt,path string,flag int,perm os.FileMode)(bool bool,errInfo error){
	if flag == 0{
		flag = os.O_CREATE|os.O_TRUNC|os.O_WRONLY
	}
	if perm == 0{
		flag = 0666
	}
	file, err := os.OpenFile(path, flag, perm)

	if err != nil {
		fmt.Println("open file failed, err:", err)
		return false,err
	}
	defer file.Close()
	//str := "hello 沙河"
	//file.Write([]byte(str))       //写入字节切片数据
	//file.WriteString("hello 小王子") //直接写入字符串数据
	_,err = file.WriteString(txt) //直接写入字符串数据
	if err != nil{
		return false,err
	}
	return true,errInfo
}


//------------------------判断文件是否存在------------------------//

//判断文件是否存在
//go中判断一个文件或者文件夹是否存在方法为:os.Stat() ,通过对返回的错误值进行判断
//1.如果err的值为nil,说明文件或文件夹存在
//2.如果返回的错误类型 使用 os.IsNotExist() 判断为true,说明文件或文件夹不存在
//3.如果返回的错误为其他类型,则不确定是否存在
//所以封装一个函数,用来判断 文件或文件夹是否存在
/*
	func  	判断文件或目录是否存在
	link 原文链接:https://blog.csdn.net/leo_jk/article/details/118255913
	author  zydh[朝游东海]
	param 	path string 		文件路径
	return	bool	返回信息
*/

func FileOrDirIsExist(path string)(bool bool){
	_,err:= os.Stat(path)
	if err == nil{
		return true
	}

	if os.IsNotExist(err){
		return false
	}
	return false

	//通过stat的isDir还可以判断一个路径是文件夹还是文件
	//stat.IsDir()
}

//------------------------删除文件------------------------//

/*
	func  	根据文件路径删除文件
	author  zydh[朝游东海]
	param 	path string 		文件路径
	return	bool	返回信息
*/
func DelFileByPath(path string)(bool bool,errInfo error){
	if FileOrDirIsExist(path){
		err := os.Remove(path)
		if err != nil{
			return false,err
		}
		return true,errInfo
	}
	return true,errInfo
}

//文件重命名 - ? 毫无用处
func FileRename(oldName,newName string)(bool,error){
	err := os.Rename(oldName,newName)
	if err != nil{
		return false,err
	}
	return true,nil
}

//GetFileLastPath 获取文件路径 ./zydh/log/log.txt --> ./zydh/log/
func GetFileLastPath(path string) string{
	//分隔
	splice := strings.Split(path, "/")
	return strings.Join(splice[:len(splice)-1],"/")
}


//------------------------切割文件------------------------//
/*
	1.按文件大小切割
	2.按日期切割
*/



/*
	func:FileCutBySize 按文件大小切割  每次记录前判断这个文件的大小
	param : path string 文件路径
	analysis:
		////os.Rename("./aa/bb/c1/file.go", "./aa/bb/c2/file.go")
		//err := os.Rename(pName,logName)  //会把文件移动到/根目录下
 */
func FileCutBySize(path string)string{
	//判断文件是否超出大小
	if GetFileSizeByPath(path) >= LOG_FILE_MAX_SIZE{
		//2.备份  xx.log -> xx.log.back202205071155  // warning.txt.bak20220507122541.973
		//在原目录备份							 /		warning.txt				warning.txt.bak20220507122541.973
		err := os.Rename(GetFileLastPath(path) + "/" + GetFileNameByPath(path),getLogFilePathByTime(path,1));if err != nil{
			fmt.Println("zydhfile/file.go FileCutBySize  Backup failed")
		}
	}
	return path
}



//FileCutByTime 按日期切割
/*
	方法一如下
	方法二,在LogToFile{}结构体中新增字段,oldPathFileName,把最新的文件名存入字段,然后在程序中和现在的时间比较,不用查
*/
func FileCutByTime(path string)string{
	//返回路径
	return getLogFilePathByTime(path,2)
}

/*
	func : 获取新的日志文件路径
	param : path string 原路径
	param : scene int 场景 1 按文件大小 2 按日期
	return : string 根据时间拼接的新路径
*/
func getLogFilePathByTime(path string,scene int) string{
	//文件路径和名词
	pName := GetFileNameByPath(path) //warning.txt
	var nowStr,logName string
	if scene == 1{
		nowStr = time.Now().Format(CUT_FILE_BY_SIZE_TIME_FORMAT) //20220507122541.973
		logName = fmt.Sprintf("%s.bak%s",pName,nowStr) //拼接一个备份
	}else if scene == 2{
		nowStr = time.Now().Format(CUT_FILE_BY_DATE_TIME_FORMAT) //20220507122541 //按秒分隔
		logName =  fmt.Sprintf("[%s]%s",nowStr,pName) //拼接一个备份
	}

	//[2022-05-07-12:25:41]warning.txt
	return GetFileLastPath(path) + "/" +logName
	// ./zydhlog/[2022-05-07-12:25:41]warning.txt
}

  

posted @ 2022-05-24 23:36  朝游东海  阅读(472)  评论(0)    收藏  举报