开源丨GOKU文件日志功能设计

网友投稿 288 2023-05-17


风靡全球的小说人物夏洛克·福尔摩斯,善于用观察与演绎推理和法学知识来解决问题,无论大小案件他总能快速的找到线索,并顺藤摸瓜找到幕后凶手。

而在现实的软件开发过程中,程序员会在代码里打上”线索”,在程序出现错误时,通过查看这些”线索”来快速定位到问题所在并继而解决——这些”线索”就是日志。

日志是开发为了跟踪用户行为和代码异常而打的记录,而文件日志则是指将日志存入到指定文件这一功能。

在开发环境中,我们会在程序中设置断点或打印日志的方式进行调试。有人只用设置断点的方式,也有人会结合两种方法,通过分析日志文件缩小断点范围来快速定位问题代码。但在生产环境里,我们无法设置断点来debug,只能通过日志信息来捕捉错误。

GOKU文件日志的实现考虑

golang实现的日志框架有很多,包括logruszapzerologseelog等,其中logrus是目前go生态最流行的日志框架。logrus兼容golang标准库log,囊括了标准库的所有日志等级,并且具有可扩展的hook机制、能选择日志的格式、内置JSONFormatter和TextFormatter。当然开发者也能自己实现接口Formatter,同时还有Field机制能够进行精细化、结构化的日志记录。美中不足的是,logrus自身没有实现日志分割的功能,而是通过hook机制间接实现。

虽然logrus框架功能丰富,性能也不错,但每个项目都有自身的需求,即使是再好的日志框架也不能生搬硬套到项目里。于是GOKU在logrus框架的基础上进行二次开发,只保留基本功能。

我们比较常见的日志框架以及根据自己的需求,归纳了以下需求点:

日志分级

事有轻重缓急,日志信息也有重要与不重要之分。设置合理的日志等级能够帮助我们过滤不重要的日志信息,快速排查问题。

按照重要程度,将日志等级分成了五个等级:

日志等级 含义 fatal 会导致程序退出的严重错误 error 一般的错误信息 warn 警告信息 info 一般信息 debug 调试信息

当文件日志设置了某个日志等级,那么程序只会输出不低于当前等级的日志信息到文件中。

在产品上线时只需要开启error等级的日志,在程序出现了不知名的错误时,则启动debug模式对程序进行排查调试,可以避免程序运行时的日志冗余。

日志分割

日志分割即每隔一段时间产生一个新的日志文件,防止所有日志记录全部堆积在一个日志文件里。

实行分割的好处有两个:

避免单个日志文件占用磁盘空间过大,查看时影响服务器IO; 能够根据时间间隔更快地定位日志;

常见的日志分割的周期有小时和天,而月、年可能会造成单个日志数据量过大,增加排查难度,所以一般不会使用。

日志定时清理

随着时间的推移,旧日志文件会越积越多,而磁盘空间有限,一旦空间消耗完,可能会导致某些应用奔溃,影响生产。

定时清理日志的优点:

无需手动删除旧日志文件,减少人力维护成本;  避免过期日志文件的堆积,既减少磁盘占用又可更快定位所需的日志文件;

文件日志的配置参数

参考功能的设计,需要有个字段来确定日志等级,同时日志文件的分割周期和保存时间也需要两个参数来配置。

接下来对文件日志功能需要配置的参数进行说明:

参数名 参数类型 参数取值范围 说明 dir 字符串 任意字符串 日志文件的目录路径(支持绝对路径和相对路径) file 字符串 任意字符串 日志文件的文件名 level 枚举 [“fatal”,”error”,”warn”,”info”,”debug”] 日志等级 perio 枚举 [“day”,”hour”] 日志文件的分割周期 expire 整型 大于0的整数 旧日志文件的保存时间,单位为天

period参数:用来设置日志文件的分割周期,即每隔一段时间就生成新的日志文件,对日志记录进行分流,以此来实现日志分割功能。可以是一天生成一个新日志文件,也可以是每小时。

expire参数:设定旧日志文件的保存时间,可以设置若干天。保存时间从新日志文件生成的那一刻开始生效,旧文件名后缀加上时间戳。程序定时检查旧文件是否过期,以此达到日志定时清理的目的。

下图以文件分割周期:day,旧文件保存时间:3天为例,对日志文件的分割及清理流程进行解释说明。

文件日志的核心代码实现

日志分级

在调用打印日志接口后最终会进入到这个方法里,entry入参是日志记录的结构体变量,而transporter是日志输出器:

//Transport 能将判断日志记录entry的等级并格式化输出

func (t *Transporter) Transport(entry *Entry) error {

       output := t.output

       if output == nil {

              return nil

       }

     //当配置的日志等级大于或等于日志记录的等级时进行输出

       if t.Level() >= entry.Level {

    //对日志记录格式化

              data, err := t.formatter.Format(entry)

              if err != nil {

                     return err

              }

    //输出日志记录

              _, err = output.Write(data)

              return err

       }

       return nil

}

日志定时清理

日志定时清理的逻辑很简单:获取日志目录下的旧日志文件,逐个判断是否过期。

在文件分割时会执行以下的方法:

//dropHistory 检查日志目录下的历史日志文件,若过期则删除

func (w *FileWriterByPeriod) dropHistory() {

       expire := w.getExpire()

       expireTime := time.Now().Add(-expire)

       pathPatten := filepath.Join(w.dir, fmt.Sprintf(“%s-*”, w.file))

       files, err := filepath.Glob(pathPatten)

       if err == nil {

              for _, f := range files {

                     if info, e := os.Stat(f); e == nil {

                            if expireTime.After(info.ModTime()) {

                                   _ = os.Remove(f)

                            }

                     }

              }

       }

}

日志分割

实现日志分割的重点在于处理的时机。定义一个时间戳变量,并且设置一个定时器,定时获取当前时间戳,当新旧时间戳不一致时才进行文件处理。核心代码如下:

case <-t.C:

   {

      if buf.Buffered() > 0 {

         buf.Flush()

         tflusth.Reset(time.Second)

      }

          //获取新时间戳,并与旧时间戳进行比较,若不一致则进行日志分割

      if lastTag != w.timeTag(time.Now()) {

         //关闭旧日志文件

         f.Close()

         //对旧日志文件重命名

         w.history(lastTag)

         //创建新日志文件

         fnew, tag, err := w.openFile()

         if err != nil {

            return

         }

         //保存新时间戳

         lastTag = tag

         f = fnew

         buf.Reset(f)

         go w.dropHistory()

      }

   }

那么时间戳是怎样的,又是如何获取的呢?

事实上,上面的时间戳是由完整时间戳格式化得到的,而具体的格式化字符串又是依据period的参数而不同。这样就能轻易获取截止到小时或天的时间戳,以此做到隔天或间隔小时生成新日志文件。

代码如下:

func (w *FileWriterByPeriod) timeTag(t time.Time) string {

   w.locker.Lock()

   //w.period.FormatLayout 获取period的格式化字符串

   tag := t.Format(w.period.FormatLayout())

   w.locker.Unlock()

   return tag

}

func (period LogPeriodType) FormatLayout() string {

   switch period {

   case PeriodHour:

      {

         return “2006-01-02-15”

      }

   case PeriodDay:

      {

         return “2006-01-02”

      }

   default:

      return “2006-01-02-15”

   }

}

最后

综上所述,goku网关基于logrus框架进行二次开发,对日志分割功能进行了完善,同时提供了日志定时清理、日志分级等操作,满足了goku网关文件日志打印的需求。另外,除了文件日志,goku还支持httplog、syslog、stdlog等多样的日志输出,这些离不开我们goku自己实现的日志框架,对多样的日志输出进行分流,下一篇我们再给大家介绍我们的日志框架的实现细节及相关考虑。

什么是GoKu网关?

Goku API Gateway (中文名:悟空 API 网关)是一个基于 Golang开发的微服务网关

,能够实现高性能 HTTP API 转发、服务编排、多租户管理、API 访问权限控制等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行 API 服务治理、提高 API 服务的稳定性和安全性。

GoKu Github地址:https://github.com/eolinker/goku

关于 Eolinker

Eolinker(Easy & Open Linker)是国内 API 接口全生命周期管理解决方案的领军者,是国内最大的在线 API 接口管理平台,也是唯一为工信部ITSS协会制定API研发管理与测试规范的企业。 Eolinker 旗下拥有 API 研发管理API 自动化测试API 微服务网关API 网络监控API 快速生成API 开放平台等多个标准化产品。 Eolinker 为全球超过3万家企业提供专业的API相关解决方案,客户遍布互联网、金融、安全、人工智能、企业服务、制造业、物联网、政府等数十个行业。

联系我们

官方网站:https://eolinker.com 市场合作:market@eolinker.com 购买咨询:sales@eolinker.com 中国大陆支持电话:400-616-0330 电话接听时间:工作日 9:30-18:00

部分客户

投资机构


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:答疑 | EOLINKER 第七期 Q&A 答疑手册
下一篇:答疑 | EOLINKER 第六期 Q&A 答疑手册
相关文章

 发表评论

暂时没有评论,来抢沙发吧~