前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >构建Golang日志组件

构建Golang日志组件

原创
作者头像
nickxuewu
修改2019-07-01 11:25:58
3K1
修改2019-07-01 11:25:58
举报
文章被收录于专栏:程序开发程序开发

背景

组内目前在构建中台能力,开发语言从C++转向golang,需要开发一款类似uls一样的日志组件

Golang日志库

golang中,流行的日志框架包括logrus、zap、zerolog、seelog等。而logrus是目前Github上star数量最多的日志库。logrus功能强大,性能高效,而且具有高度灵活性,提供了自定义插件的功能.很多开源项目

Logrus特点

ØFields:logrus鼓励通过Field机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志

ØHook机制:允许使用者通过hook的方式将日志分发到任意地方,如本地文件系统、标准输出、fluentd、logstash、elasticsearch或者mq等,也可以通过hook自定义日志内容和格式等

Ø六种日志级别:debug、info、warn、error、fatal和panic,API完全兼容标准包logger

ØEntries:logrus.WithFields会自动返回一个*Entry,Entry里面的有些变量会被自动加上,比如time(entry被创建时的时间戳)、msg(在调用.Info()等方法时被添加)、level(日志级别)

ØFormatters:支持TextFormatter和JSONFormatter,还有其他第三方日志输出格式,比如FluentdFormatter和logstash等

Ø线程安全:日志并发写操作通过mutex进行保护的

Logrus不足

1)没有提供行号和文件名的支持

2)输出到本地文件系统没有提供日志分割功能

3)没有提供输出到EFK等日志处理中心的功能

实现

1、增加行号和文件名的支持

针对Logrus的不足,利用logrus的可扩展的hook特性,实现自定义的hook。

Hook接口的定义如下

type Hook interface {

Levels() []Level

Fire(*Entry) error

}

主要实现Hook接口的Fire方法,Fire实现如下

func (hook *FileHook) Fire(entry *logrus.Entry) error {

data := make(map[string]interface{})

data["service"] = hook.tag

for k, v := range entry.Data {

data[k] = v

}

data["time"] = time.Now().UTC().Format("2006-01-02 15:04:05.000000 MST")

data["level"] = entry.Level.String()

file, fn, line := utils.GetCallerIgnoreFiles(7)

fileSplit := strings.Split(file, "/")

length := len(fileSplit)

fileName := ""

if length > 3 {

fileName = strings.Join(fileSplit[length-3:], "/")

} else {

fileName = file

}

fnSplit := strings.Split(fn, ".")

data["file"] = fileName + ":" + strconv.Itoa(line)

data["func"] = fnSplit[len(fnSplit)-1]

data["msg"] = entry.Message

result, err := json.Marshal(data)

if err != nil {

fmt.Printf("log marshal failed, err: %v\n", err)

return err

}

result = append(result, '\r', '\n')

_, err = hook.writer.Write(result)

return err

}

从上述代码可以看出,主要增加了file、func、line、service等必要字段数据,另外通过调用log.WithFields(field),可以动态自定义添加需要的字段数据到日志中。

2、增加日志切割功能

引入file- rotatelogs(代码路径见附录)

func New(baseLogPath string) (*FileHook, error) {

basename := filepath.Base(baseLogPath)

if basename == "." {

return nil, errors.New("error log path")

}

utils.CreateDir(baseLogPath)

fileName := fmt.Sprintf("%s/%s", baseLogPath, basename)

linkName := fmt.Sprintf("%s/%s_water_0_.log", baseLogPath, basename)

writer, err := rotatelogs.New(

fileName+"_%Y%m%d%H%M_.log",

rotatelogs.WithLinkName(linkName), // 生成软链,指向最新日志文件

//rotatelogs.WithMaxAge(7*24*time.Hour), // 文件最大保存时间

rotatelogs.WithRotationCount(50), // 最多文件数 WithMaxAge 与WithRotationCount 二选一

rotatelogs.WithRotationTime(1*time.Hour), // 日志切割时间间隔

)

if err != nil {

log.Errorf("config local file system logger error. %v", errors.WithStack(err))

}

return &FileHook{writer: writer}, nil

}

在自定义hook的New()方法中加入file-rotate的相关实例化代码,实现日志按时间切割,保留多少个日志文件(也可以设置日志文件最大保留时间,超期进行清理)

3、增加按文件大小切割日志的功能

使用过程中发现,不同的时间段,产生的log数据量不同,导致有的文件比较大,有的比较小,文件大小差异可能比较大。

因此在file- rotatelogs的基础上增加了按日志文件大小进行切割日志文件的功能

主要的实现代码如下

func (rl *RotateLogs) genTimeBaseFilename() string {

now := rl.clock.Now()

// XXX HACK: Truncate only happens in UTC semantics, apparently.

// observed values for truncating given time with 86400 secs:

//

// before truncation: 2018/06/01 03:54:54 2018-06-01T03:18:00+09:00

// after truncation: 2018/06/01 03:54:54 2018-05-31T09:00:00+09:00

//

// This is really annoying when we want to truncate in local time

// so we hack: we take the apparent local time in the local zone,

// and pretend that it's in UTC. do our math, and put it back to

// the local zone

var base time.Time

if now.Location() != time.UTC {

base = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.UTC)

base = base.Truncate(time.Duration(rl.rotationTime))

base = time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), base.Minute(), base.Second(), base.Nanosecond(), base.Location())

} else {

base = now.Truncate(time.Duration(rl.rotationTime))

}

return rl.pattern.FormatString(base)

}

func (rl *RotateLogs) genFilename() string {

// rotate each rotationTime

if rl.rotationTime > 0 {

return rl.genTimeBaseFilename() + rl.ext

}

// first time

if rl.curFn == "" {

rl.curFn = rl.genTimeBaseFilename() + rl.ext

fh, err := os.OpenFile(rl.curFn, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)

if err != nil {

return rl.curFn

}

if rl.outFh != nil {

rl.outFh.Close()

}

rl.outFh = fh

}

fs, err := rl.outFh.Stat()

if err != nil {

return rl.curFn

}

// 超过设置的文件大小的阈值

if fs.Size() >= int64(rl.rotationSize) { // 生成新文件

filename := rl.genTimeBaseFilename()

generation := 0

for {

name := fmt.Sprintf("%s_%d%s", filename, generation, rl.ext)

if _, err := os.Stat(name); err != nil {

filename = name

break

}

generation++

}

return filename

}

return rl.curFn

}

最后自定义hook的New()方法如下

func New(baseLogPath string) (*FileHook, error) {

basename := filepath.Base(baseLogPath)

if basename == "." {

return nil, errors.New("error log path")

}

utils.CreateDir(baseLogPath)

fileName := fmt.Sprintf("%s/%s", baseLogPath, basename)

linkName := fmt.Sprintf("%s/%s_water_0_.log", baseLogPath, basename)

writer, err := rotatelogs.New(

fileName+"_%Y%m%d%H%M",

".log",

rotatelogs.WithLinkName(linkName), // 生成软链,指向最新日志文件

rotatelogs.WithMaxAge(-1), // 文件最大保存时间

rotatelogs.WithRotationCount(50), // 最多文件数 WithMaxAge 与WithRotationCount 二选一

rotatelogs.WithRotationTime(-1), // 日志切割时间间隔

rotatelogs.WithRotationSize(8*1024*1024), // 日志切割大小 WithRotateTime 与WithRotationSize 二选一

)

if err != nil {

log.Errorf("config local file system logger error. %v", errors.WithStack(err))

}

return &FileHook{writer: writer}, nil

}

结果

日志目录如下

日志内容如下

我们动态自定义了clientid、uid、traceid等字段。

附录

logrus:https://github.com/sirupsen/logrus

file-rotate:https://github.com/lestrrat-go/file-rotatelogs

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • Golang日志库
  • Logrus特点
  • Logrus不足
  • 实现
    • 1、增加行号和文件名的支持
      • 2、增加日志切割功能
        • 3、增加按文件大小切割日志的功能
        • 结果
        • 附录
        相关产品与服务
        日志服务
        日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档