组内目前在构建中台能力,开发语言从C++转向golang,需要开发一款类似uls一样的日志组件
golang中,流行的日志框架包括logrus、zap、zerolog、seelog等。而logrus是目前Github上star数量最多的日志库。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进行保护的
1)没有提供行号和文件名的支持
2)输出到本地文件系统没有提供日志分割功能
3)没有提供输出到EFK等日志处理中心的功能
针对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),可以动态自定义添加需要的字段数据到日志中。
引入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的相关实例化代码,实现日志按时间切割,保留多少个日志文件(也可以设置日志文件最大保留时间,超期进行清理)
使用过程中发现,不同的时间段,产生的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 删除。