欢迎关注「Keegan小钢」公众号获取更多文章缓存
撮合引擎开发:开篇数据结构
撮合引擎开发:对接黑箱post
咱们都知道日志在一个程序中有着重要的做用,撮合引擎也一样须要一个完善的日志输出功能,以方便调试和查询数据。spa
对一个撮合引擎来讲,须要输出的日志主要有如下几类:设计
另外,撮合引擎产生的日志会很是多,因此还应该作日志分割,按日期分割是最经常使用的日志分割方式,因此咱们也一样将不一样日期的日志分割到不一样日志文件保存。
首先,咱们都知道日志是有分级别的,多的好比 log4j 定义了 8 种级别的日志。不过,最经常使用的就 4 种级别,优先级从低到高分别为:DEBUG、INFO、WARN、ERROR。通常,不一样环境会设置不一样的日志级别,如 DEBUG 级别通常只在开发和测试环境才设置,生产环境则会设置为 INFO 或更高级别。当设置为高级别时,低级别的日志消息是不会打印出来的。那为了打印不一样级别的日志消息,能够提供不一样级别的打印函数,好比提供 log.Debug()、log.Info() 等函数。
其次,日志须要输出到文件保存,所以,就须要指定文件保存的目录、文件名和文件对象。通常,保存的文件目录和运行程序应该放在一块儿,因此,指定的文件目录最好是相对路径。
另外,文件还要根据日期作分割,即不一样日期的日志消息要保存到不一样的日志文件,那么,天然要记录下当前日志的日期。以及须要定时监控,当检测到最新日期跟当前日志的日期相比已经跨日了,说明须要进行日志分割了,那就将当前的日志文件进行备份,并建立新文件用来保存新日期的日志消息。
最后,日志消息写入文件的话,那就少不了耗时的 I/O 操做,若是用同步方式写日志,无疑会减低撮合性能,所以,最好选用异步方式写日志,能够用带缓冲的通道实现。
我从新自定义了一个 log 包,并建立了 log.go 文件,全部代码都写在该文件中。
第一步,先定义几种日志等级,直接定义成枚举类型,以下:
type LEVEL byte
const (
DEBUG LEVEL = iota
INFO
WARN
ERROR
)
复制代码
第二步,定义日志的结构体,其包含的字段比较多,以下:
type FileLogger struct {
fileDir string // 日志文件保存的目录
fileName string // 日志文件名(无需包含日期和扩展名)
prefix string // 日志消息的前缀
logLevel LEVEL // 日志等级
logFile *os.File // 日志文件
date *time.Time // 日志当前日期
lg *log.Logger // 系统日志对象
mu *sync.RWMutex // 读写锁,在进行日志分割和日志写入时须要锁住
logChan chan string // 日志消息通道,以实现异步写日志
stopTickerChan chan bool // 中止定时器的通道
}
复制代码
第三步,为了能将日志应用到程序中任何地方,就须要定义一个全局的日志对象,并要对该日志对象进行初始化。初始化操做有一点复杂,咱们先来看代码:
const DATE_FORMAT = "2006-01-02"
var fileLog *FileLogger
func Init(fileDir, fileName, prefix, level string) error {
CloseLogger()
f := &FileLogger{
fileDir: fileDir,
fileName: fileName,
prefix: prefix,
mu: new(sync.RWMutex),
logChan: make(chan string, 5000),
stopTikerChan: make(chan bool, 1),
}
switch strings.ToUpper(level) {
case "DEBUG":
f.logLevel = DEBUG
case "WARN":
f.logLevel = WARN
case "ERROR":
f.logLevel = ERROR
default:
f.logLevel = INFO
}
t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
f.date = &t
f.isExistOrCreateFileDir()
fullFileName := filepath.Join(f.fileDir, f.fileName+".log")
file, err := os.OpenFile(fullFileName, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return err
}
f.logFile = file
f.lg = log.New(f.logFile, prefix, log.LstdFlags|log.Lmicroseconds)
go f.logWriter()
go f.fileMonitor()
fileLogger = f
return nil
}
复制代码
这个初始化的逻辑有点多,我来进行拆分讲解。首先,第一步,调用了 CloseLogger() 函数,该函数主要是关闭文件、关闭通道等操做。为了中止一个不断循环的 goroutine,关闭通道是一个经常使用的方案,这在以前的文章也有说过。那么,因为初始化函数能够会被调用屡次,以实现配置的变动,那若是不先结束旧的 goroutine ,那一样功能的 goroutine 将不止一个在同时运行,这无疑将会出问题。所以,须要先关闭 Logger,关闭 Logger 的代码以下:
func CloseLogger() {
if fileLogger != nil {
fileLogger.stopTikerChan <- true
close(fileLogger.stopTikerChan)
close(fileLogger.logChan)
fileLogger.lg = nil
fileLogger.logFile.Close()
}
}
复制代码
关闭 Logger 以后,就是对一些字段的初始化赋值了,其中,f.date 设置为了当前日期,后面判断是否须要分割就以这个日期为条件。f.isExistOrCreateFileDir() 则会判断日志目录是否存在,若是不存在则会建立该目录。接着,将目录、设置的文件名和添加的 .log 文件扩展名拼接在一块儿,拼接出文件的完整名字并打开文件。以后就是用该文件来初始化系统日志对象 f.lg 了,将日志消息写入文件时其实就是调用该对象的 Output() 函数。后面启动了两个 goroutine:一个用来监听 logChan,实现将日志消息写入文件;一个用来定时监听文件是否须要分割,须要分割时则实现分割。
接着,咱们就来看看这两个 goroutine 的实现:
func (f *FileLogger) logWriter() {
defer func() { recover() }()
for {
str, ok := <-f.logChan
if !ok {
return
}
f.mu.RLock()
f.lg.Output(2, str)
f.mu.RUnlock()
}
}
func (f *FileLogger) fileMonitor() {
defer func() { recover() }()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if f.isMustSplit() {
if err := f.split(); err != nil {
Error("Log split error: %v\n", err)
}
}
case <-f.stopTikerChan:
return
}
}
}
复制代码
能够看到 logWriter() 循环从 logChan 通道读取日志消息,当通道被关闭则退出,不然就调用 f.lg.Output() 将日志输出。fileMonitor() 里则建立了一个每隔 30 秒发送一次的 ticker,当从 ticker.C 接收到数据以后,就判断是否须要分割,若是须要则调用分割函数 f.split()。而从 f.stopTikerChan 收到数据时,说明该定时器也要结束了。
接着,再来看看 isMustSplit() 和 split() 函数了。isMustSplit() 很是简单,就两行代码,以下:
func (f *FileLogger) isMustSplit() bool {
t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
return t.After(f.date)
}
复制代码
split() 则复杂些,首先对日志要先加写锁,避免分割时依然有日志写入,接着对当前的日志文件进行重命名备份,而后生成新文件用来记录新的日志消息,并将当前的全局日志对象指向新文件、新日期和新的系统日志对象。实现代码以下:
func (f *FileLogger) split() error {
f.mu.Lock()
defer f.mu.Unlock()
logFile := filepath.Join(f.fileDir, f.fileName)
logFileBak := logFile + "-" + f.date.Format(DATE_FORMAT) + ".log"
if f.logFile != nil {
f.logFile.Close()
}
err := os.Rename(logFile, logFileBak)
if err != nil {
return err
}
t, _ := time.Parse(DATE_FORMAT, time.Now().Format(DATE_FORMAT))
f.date = &t
f.logFile, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
return err
}
f.lg = log.New(f.logFile, f.prefix, log.LstdFlags|log.Lmicroseconds)
return nil
}
复制代码
最后,就剩下定义一些接收日志消息的函数了,实现都很简单,以 Info() 为例:
func Info(format string, v ...interface{}) {
_, file, line, _ := runtime.Caller(1)
if fileLogger.logLevel <= INFO {
fileLogger.logChan <- fmt.Sprintf("[%v:%v]", filepath.Base(file), line) + fmt.Sprintf("[INFO]"+format, v...)
}
}
复制代码
Debug()、Warn()、Error() 等函数都相似的,照猫画虎便可。
至此,咱们这个可以实现按日期分割日志文件的日志包就完成了,剩下的,就在对应须要添加日志输出的地方调用响应的日志等级函数便可。
本小结的核心实际上是增长了一个通用的日志包,该日志包不只能够用在咱们的撮合引擎,也能用于其余项目。若是再将其扩展,还能够改成按其余条件分割,好比按小时分割,或按文件大小分割。有兴趣的小伙伴能够本身去尝试一下。
今日的思考题:要实现接口的请求和响应数据进行统一的日志输出,有哪些方案?
扫描如下二维码便可关注公众号(公众号名称:Keegan小钢)