GO 语言框架中如何快速集成日志模块

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 在我们的日常开发中, 日志模块永远是最基础且最重要的一个模块, 它可以有效的帮我们发现问题, 定位问题, 最后去解决问题;

前言

在我们的日常开发中, 日志模块永远是最基础且最重要的一个模块, 它可以有效的帮我们发现问题, 定位问题, 最后去解决问题;

zap包的集成

简介

zap是一个可以在go项目中进行快速, 结构化且分级的日志记录包, git star数高达16.3k, Git 项目地址, 在各大公司项目中被广泛使用;

最基础的使用

package main

import (
    "go.uber.org/zap"
    "time"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    logger.Info(" log info msg",
        zap.String("name", "阿里云"),
        zap.Int("num", 3),
        zap.Duration("timer", time.Minute),
    )
}
{"level":"info","ts":1657600159.826612,"caller":"log/main.go:11","msg":" log info msg","name":"阿里云","num":3,"timer":60}
{"level":"warn","ts":1657600159.8266969,"caller":"log/main.go:16","msg":" this is err msg","msg":"code err"}

可以看到上面就是打印出来的log, 当然这是用的默认配置, 所以格式和输出可能不太符合我们的要求, 我们可以根据自己的需求去修改配置来完成定制化log;

定制化

// NewProduction builds a sensible production Logger that writes InfoLevel and
// above logs to standard error as JSON.
//
// It's a shortcut for NewProductionConfig().Build(...Option).
func NewProduction(options ...Option) (*Logger, error) {
   return NewProductionConfig().Build(options...)
}

可以看到生成log的方法其实就是用 configbuild 来构造一个记录器, 我们试试自定义一下;

package main

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "time"
)

func main() {
    conf := zap.NewProductionConfig()

    // 可以把输出方式改为控制台编码, 更容易阅读
    conf.Encoding = "console"
    // 时间格式自定义
    conf.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
        enc.AppendString("[" + t.Format("2006-01-02 15:04:05") + "]")
    }
    // 打印路径自定义
    conf.EncoderConfig.EncodeCaller = func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
        encoder.AppendString("[" + caller.TrimmedPath() + "]")
    }
    // 级别显示自定义
    conf.EncoderConfig.EncodeLevel = func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
        encoder.AppendString("[" + level.String() + "]")
    }

    logger, _ := conf.Build()
    logger.Info("service start")

    logger.Info("info msg",
        zap.String("name", "阿里云"),
        zap.Int("num", 3),
        zap.Duration("timer", time.Minute),
    )
}
[2022-07-12 14:57:18]   [info]  [log/main.go:28]        service start
[2022-07-12 14:57:18]   [info]  [log/main.go:30]        info msg        {"name": "阿里云", "num": 3, "timer": 60}

这种日志一般大家看起来就比较舒服了, 特别是打印json结构的话可以直接复制解析器里面去看, 没有 json编码的各种转义, 适合平时开发使用, json结构的则适合生产环境, 方便用elk进行日志收集统计;

zap包还是很灵活的, 基本上配置参数都支持自定义(输出键名, 格式等等), 大家可以按需配置使用;

// An EncoderConfig allows users to configure the concrete encoders supplied by
// zapcore.
type EncoderConfig struct {
    // Set the keys used for each log entry. If any key is empty, that portion
    // of the entry is omitted.
    MessageKey    string `json:"messageKey" yaml:"messageKey"`
    LevelKey      string `json:"levelKey" yaml:"levelKey"`
    TimeKey       string `json:"timeKey" yaml:"timeKey"`
    NameKey       string `json:"nameKey" yaml:"nameKey"`
    CallerKey     string `json:"callerKey" yaml:"callerKey"`
    FunctionKey   string `json:"functionKey" yaml:"functionKey"`
    StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
    LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
    // Configure the primitive representations of common complex types. For
    // example, some users may want all time.Times serialized as floating-point
    // seconds since epoch, while others may prefer ISO8601 strings.
    EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
    EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
    EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
    EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
    // Unlike the other primitive type encoders, EncodeName is optional. The
    // zero value falls back to FullNameEncoder.
    EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
    // Configures the field separator used by the console encoder. Defaults
    // to tab.
    ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

进阶封装

项目里面生产环境为了方便收集日志可能都比较喜欢用json, 开发环境中大家又都比较喜欢console方便调试, 还有比如希望接口/服务日志带上属于自己的唯一请求标识这种, 以及对日志进行分类保存等等, 就需要对zap包进行再一次的封装;

下面是我自己封装的log包代码, 可以很方便的解决上面的问题, 大家可以当做参考;

index.go 记录器初始化, 根据配置选择编码输出方式及日志文件保存逻辑, 以及一些自己自定义的输出标准;
package logging

import (
    "go.uber.org/zap"
    "go.uber.org/zap/buffer"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
    "os"
    "path"
    "strings"
)

type (
    Conf struct {
        Path    string // 日志路径
        Encoder string // 编码器选择
    }
    logItem struct {
        FileName string
        Level    zap.LevelEnablerFunc
    }
    Encoder interface {
        Config() zapcore.Encoder
        WithKey(key string) Encoder
        WithField(key, val string) Encoder
        Debug(msg string)
        Debugf(format string, v ...interface{})
        Info(msg string)
        Infof(format string, v ...interface{})
        Warn(msg string)
        Warnf(format string, v ...interface{})
        Error(msg string)
        Errorf(format string, v ...interface{})
        Fatal(msg string)
        Fatalf(format string, v ...interface{})
    }
)

var (
    maxSize    = 200 // 每个日志文件最大尺寸200M
    maxBackups = 20  // 日志文件最多保存20个备份
    maxAge     = 30  // 保留最大天数
    _logger    *zap.Logger
    _pool      = buffer.NewPool()
    c          Conf

    ConsoleEncoder = "console" // 控制台输出
    JsonEncoder    = "json"    // json输出
)

// Init 初始化日志.
func Init(conf Conf) {
    c = conf
    prefix, suffix := getFileSuffixPrefix(c.Path)

    infoPath := path.Join(prefix + ".info" + suffix)
    errPath := path.Join(prefix + ".err" + suffix)
    items := []logItem{
        {
            FileName: infoPath,
            Level: func(level zapcore.Level) bool {
                return level <= zap.InfoLevel
            },
        },
        {
            FileName: errPath,
            Level: func(level zapcore.Level) bool {
                return level > zap.InfoLevel
            },
        },
    }

    NewLogger(items)
}

// NewLogger 日志.
func NewLogger(items []logItem) {
    var (
        cfg   zapcore.Encoder
        cores []zapcore.Core
    )
    switch c.Encoder {
    case JsonEncoder:
        cfg = NewJsonLog().Config()
    case ConsoleEncoder:
        cfg = NewConsoleLog().Config()
    default:
        cfg = NewConsoleLog().Config()
    }

    for _, v := range items {
        hook := lumberjack.Logger{
            Filename:   v.FileName,
            MaxSize:    maxSize,    // 每个日志文件保存的最大尺寸 单位:M
            MaxBackups: maxBackups, // 日志文件最多保存多少个备份
            MaxAge:     maxAge,     // 文件最多保存多少天
            Compress:   true,       // 是否压缩
            LocalTime:  true,       // 备份文件名本地/UTC时间
        }
        core := zapcore.NewCore(
            cfg, // 编码器配置;
            zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&hook)), // 打印到控制台和文件
            v.Level, // 日志级别
        )
        cores = append(cores, core)
    }

    // 开启开发模式,堆栈跟踪
    caller := zap.AddCaller()
    // 开发模式
    development := zap.Development()
    // 二次封装
    skip := zap.AddCallerSkip(1)
    // 构造日志
    _logger = zap.New(zapcore.NewTee(cores...), caller, development, skip)
    return
}

// GetEncoder 获取自定义编码器.
func GetEncoder() Encoder {
    switch c.Encoder {
    case JsonEncoder:
        return NewJsonLog()
    case ConsoleEncoder:
        return NewConsoleLog()
    default:
        return NewConsoleLog()
    }
}

// GetLogger 获取日志记录器.
func GetLogger() *zap.Logger {
    return _logger
}

// getFileSuffixPrefix 文件路径切割
func getFileSuffixPrefix(fileName string) (prefix, suffix string) {
    paths, _ := path.Split(fileName)
    base := path.Base(fileName)
    suffix = path.Ext(fileName)
    prefix = strings.TrimSuffix(base, suffix)
    prefix = path.Join(paths, prefix)
    return
}

// getFilePath 自定义获取文件路径.
func getFilePath(ec zapcore.EntryCaller) string {
    if !ec.Defined {
        return "undefined"
    }
    buf := _pool.Get()
    buf.AppendString(ec.Function)
    buf.AppendByte(':')
    buf.AppendInt(int64(ec.Line))
    caller := buf.String()
    buf.Free()
    return caller
}
console.go 控制台编码输出, 支持自定义;
package logging

import (
    "fmt"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "time"
)

type ConsoleLog struct {
    val string
}

func NewConsoleLog() Encoder {
    return new(ConsoleLog)
}

// Config 自定义配置.
func (slf *ConsoleLog) Config() zapcore.Encoder {
    var (
        cfg = zap.NewProductionEncoderConfig()
    )

    // 时间格式自定义
    cfg.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
        enc.AppendString("[" + t.Format("2006-01-02 15:04:05") + "]")
    }
    // 打印路径自定义
    cfg.EncodeCaller = func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
        encoder.AppendString("[" + getFilePath(caller) + "]")
    }
    // 级别显示自定义
    cfg.EncodeLevel = func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
        encoder.AppendString("[" + level.String() + "]")
    }
    return zapcore.NewConsoleEncoder(cfg)
}

// WithKey 添加单个键.
func (slf *ConsoleLog) WithKey(key string) Encoder {
    slf.val = slf.val + "[" + key + "]    "
    return slf
}

// WithField 添加字段.
func (slf *ConsoleLog) WithField(key, val string) Encoder {
    slf.val = slf.val + fmt.Sprintf("[%s:%s]    ", key, val)
    return slf
}

func (slf *ConsoleLog) Debug(msg string) {
    _logger.Debug(slf.val + msg)
}

func (slf *ConsoleLog) Debugf(format string, v ...interface{}) {
    _logger.Debug(fmt.Sprintf(slf.val+format, v...))
}

func (slf *ConsoleLog) Info(msg string) {
    _logger.Info(slf.val + msg)
}

func (slf *ConsoleLog) Infof(format string, v ...interface{}) {
    _logger.Info(fmt.Sprintf(slf.val+format, v...))
}

func (slf *ConsoleLog) Warn(msg string) {
    _logger.Warn(slf.val + msg)
}

func (slf *ConsoleLog) Warnf(format string, v ...interface{}) {
    _logger.Warn(fmt.Sprintf(slf.val+format, v...))
}

func (slf *ConsoleLog) Error(msg string) {
    _logger.Error(slf.val + msg)
}

func (slf *ConsoleLog) Errorf(format string, v ...interface{}) {
    _logger.Error(fmt.Sprintf(slf.val+format, v...))
}

func (slf *ConsoleLog) Fatal(msg string) {
    _logger.Fatal(slf.val + msg)
}

func (slf *ConsoleLog) Fatalf(format string, v ...interface{}) {
    _logger.Fatal(fmt.Sprintf(slf.val+format, v...))
}
json.go json编码输出, 支持自定义;
package logging

import (
    "fmt"
    "go.uber.org/zap/zapcore"
    "time"

    "go.uber.org/zap"
)

type JsonLog struct {
    fields []zap.Field
    val    string
}

// NewJsonLog  自定义添加log field.
func NewJsonLog() Encoder {
    return &JsonLog{fields: make([]zap.Field, 0)}
}

// Config 自定义配置.
func (slf *JsonLog) Config() zapcore.Encoder {
    var (
        cfg = zap.NewProductionEncoderConfig()
    )

    // 时间格式自定义
    cfg.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
        enc.AppendString(t.Format("2006-01-02 15:04:05"))
    }
    // 打印路径自定义
    cfg.EncodeCaller = func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) {
        encoder.AppendString(getFilePath(caller))
    }
    // 级别显示自定义
    cfg.EncodeLevel = func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
        encoder.AppendString(level.String())
    }
    return zapcore.NewJSONEncoder(cfg)
}

// WithKey 添加单个键.
func (slf *JsonLog) WithKey(key string) Encoder {
    slf.val = slf.val + key + " "
    return slf
}

// WithField 添加字段.
func (slf *JsonLog) WithField(key, val string) Encoder {
    slf.fields = append(slf.fields, zap.String(key, val))
    return slf
}

func (slf *JsonLog) Debug(msg string) {
    _logger.Debug(slf.val+msg, slf.fields...)
}

func (slf *JsonLog) Debugf(format string, v ...interface{}) {
    _logger.Debug(fmt.Sprintf(slf.val+format, v...), slf.fields...)
}

func (slf *JsonLog) Info(msg string) {
    _logger.Info(slf.val+msg, slf.fields...)
}

func (slf *JsonLog) Infof(format string, v ...interface{}) {
    _logger.Info(fmt.Sprintf(slf.val+format, v...), slf.fields...)
}

func (slf *JsonLog) Warn(msg string) {
    _logger.Warn(slf.val+msg, slf.fields...)
}

func (slf *JsonLog) Warnf(format string, v ...interface{}) {
    _logger.Warn(fmt.Sprintf(slf.val+format, v...), slf.fields...)
}

func (slf *JsonLog) Error(msg string) {
    _logger.Error(slf.val+msg, slf.fields...)
}

func (slf *JsonLog) Errorf(format string, v ...interface{}) {
    _logger.Error(fmt.Sprintf(slf.val+format, v...), slf.fields...)
}

func (slf *JsonLog) Fatal(msg string) {
    _logger.Fatal(slf.val+msg, slf.fields...)
}

func (slf *JsonLog) Fatalf(format string, v ...interface{}) {
    _logger.Fatal(fmt.Sprintf(slf.val+format, v...), slf.fields...)
}
service.go 标准输出方法, 方便直接调用;
package logging

import (
    "fmt"
)

func Sync() {
    _ = _logger.Sync()
}

func Debug(msg string) {
    _logger.Debug(msg)
}

func Debugf(format string, v ...interface{}) {
    _logger.Debug(fmt.Sprintf(format, v...))
}

func Info(msg string) {
    _logger.Info(msg)
}

func Infof(format string, v ...interface{}) {
    _logger.Info(fmt.Sprintf(format, v...))
}

func Warn(msg string) {
    _logger.Warn(msg)
}

func Warnf(format string, v ...interface{}) {
    _logger.Warn(fmt.Sprintf(format, v...))
}

func Error(msg string) {
    _logger.Error(msg)
}

func Errorf(format string, v ...interface{}) {
    _logger.Error(fmt.Sprintf(format, v...))
}

func Fatal(msg string) {
    _logger.Fatal(msg)
}

func Fatalf(format string, v ...interface{}) {
    _logger.Fatal(fmt.Sprintf(format, v...))
}

上面的进阶代码摘自我开发的一个git项目中, 主要是一个Go的标准项目布局, 封装了一些常用的组件, 有兴趣的朋友可以了解一下, 对新手还是很友好的;

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
22天前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
86 1
|
2月前
|
算法 API Apache
Flink CDC:新一代实时数据集成框架
本文源自阿里云实时计算团队 Apache Flink Committer 任庆盛在 Apache Asia CommunityOverCode 2024 的分享,涵盖 Flink CDC 的概念、版本历程、内部实现及社区未来规划。Flink CDC 是一种基于数据库日志的 CDC 技术实现的数据集成框架,能高效完成全量和增量数据的实时同步。自 2020 年以来,Flink CDC 经过多次迭代,已成为功能强大的实时数据集成工具,支持多种数据库和数据湖仓系统。未来将进一步扩展生态并提升稳定性。
584 1
Flink CDC:新一代实时数据集成框架
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
51 3
Golang语言之Prometheus的日志模块使用案例
|
1月前
|
开发框架 监控 搜索推荐
GoFly快速开发框架集成ZincSearch全文搜索引擎 - Elasticsearch轻量级替代为ZincSearch全文搜索引擎
本文介绍了在项目开发中使用ZincSearch作为全文搜索引擎的优势,包括其轻量级、易于安装和使用、资源占用低等特点,以及如何在GoFly快速开发框架中集成和使用ZincSearch,提供了详细的开发文档和实例代码,帮助开发者高效地实现搜索功能。
118 0
|
2月前
|
Shell Python
salt自定义模块内使用日志例子
salt自定义模块内使用日志例子
logging 日志 模块
logging 日志 模块
|
3月前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
59 0
|
3月前
|
测试技术 持续交付 开发者
Xamarin 高效移动应用测试最佳实践大揭秘,从框架选择到持续集成,让你的应用质量无敌!
【8月更文挑战第31天】竞争激烈的移动应用市场,Xamarin 作为一款优秀的跨平台开发工具,提供了包括单元测试、集成测试及 UI 测试在内的全面测试方案。借助 Xamarin.UITest 框架,开发者能便捷地用 C# 编写测试案例,如登录功能测试;通过 Xamarin 模拟框架,则可在无需真实设备的情况下模拟各种环境测试应用表现;Xamarin.TestCloud 则支持在真实设备上执行自动化测试,确保应用兼容性。结合持续集成与部署策略,进一步提升测试效率与应用质量。掌握 Xamarin 的测试最佳实践,对确保应用稳定性和优化用户体验至关重要。
63 0
|
3月前
|
数据库 Java 数据库连接
Struts 2 与 Hibernate 的完美邂逅:如何无缝集成两大框架,轻松玩转高效 CRUD 操作?
【8月更文挑战第31天】本文通过具体示例介绍了如何在 Struts 2 中整合 Hibernate,实现基本的 CRUD 操作。首先创建 Maven 项目并添加相关依赖,接着配置 Hibernate 并定义实体类及其映射文件。然后创建 DAO 接口及实现类处理数据库操作,再通过 Struts 2 的 Action 类处理用户请求。最后配置 `struts.xml` 文件并创建 JSP 页面展示用户列表及编辑表单。此示例展示了如何配置和使用这两个框架,使代码更加模块化和可维护。
84 0
|
12天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
118 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板

热门文章

最新文章