GO-日志分析

GO-日志分析

log包简介

Go提供了logger包来做日志记录。使用方式如下所示

package mainimport ("log""os"
)func main() {// 创建一个新的日志文件.默认是stdOutfile, err := os.Create("app.log")if err != nil {log.Fatal(err)}defer file.Close()// 设置日志输出的目标为文件log.SetOutput(file)// 设置日志的前缀log.SetPrefix("LOG: ")// 设置日志的标志,例如显示日期和时间log.SetFlags(log.LstdFlags | log.Llongfile) // 默认是LstdFlags// 记录日志log.Println("This is a log message")log.Printf("This is a formatted log message: %s", "hello world")
}

使用起来很简单,并且理解起来很容易。但有下面的几个问题

  1. 不支持日志级别,并且提供的方法不是常规的使用日志的方式,比如info,debug,error 等。
  2. 性能问题。日志在项目中使用的场景很多,日志不应该成为性能瓶颈。(日志得快)
  3. 输出日志不是格式化的。
  4. 不支持滚动日志。

下面介绍一下zap

zap包简介

官网:https://github.com/uber-go/zap

zap在很多开源的库中使用,比如etcd,dubbo-go等中。

zap的优点

  • 支持完整的日志级别
  • 速度快
  • 结构性日志

zap的使用很简单,在使用的时候的流程如下:

在这里插入图片描述

代码如下:

package mainimport ("go.uber.org/zap""time"
)func main() {url := "http://example.com"// 创建一个生产环境的configconfig := zap.NewProductionConfig()// 设置输出到标准输出和文件,这里不支持滚动日志config.OutputPaths =  []string{"stdout", "zap.log"}// 构建loggerlogger, _ := config.Build()defer logger.Sync() // flushes buffer, if anysugar := logger.Sugar()sugar.Infow("failed to fetch URL",// Structured context as loosely typed key-value pairs."url", url,"attempt", 3,"backoff", time.Second,)sugar.Infof("Failed to fetch URL: %s", url)
}

并且他已经提供了几个成熟的配置,方便学习和使用。

  • NewDevelopment
  • NewProduction
  • NewExample

这三种方式,只是帮我们构建好了config和EncoderConfig,当然,我们也可以自己做,也可以在他们的基础上继续做。

它提供了两种logger对象

  • SugaredLogger

    性能和使用平衡的一个日志对象(一般直接使用它就已经ok了)

  • logger

    对性能要求很高的情况下使用的日志对象

为了搞清楚也方便理解,从源码开始分析(go官方的log包这里就不分析了,内容很简单,看一下就行)

zap源码分析

日志需要下面的几个要素

  1. 日志写到哪里?如何抽象?
  2. 日志的级别怎么处理?
  3. 如何提高速度?
  4. 日志如何编码?
  5. 如果做到线程安全?
  6. 日志输出中包含哪些关键字,如何拓展?用户可以自己添加一些关键字

config开始看,看有哪些可配置的,从而发现蛛丝马迹,来回答上面的问题,理解zap的设计

config解析

type Config struct {// 日志级别Level AtomicLevel `json:"level" yaml:"level"`// 开发模式Development bool `json:"development" yaml:"development"`DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`// 采样器的配置Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`// 编码方式Encoding string `json:"encoding" yaml:"encoding"`// 编码的配置EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`// 日志输出的位置OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`// 错误输出的位置,这只是设置的zap内部的错误输出的位置ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`// 可配置的字段InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

sink

sink就是抽象出来的日志文件的写入源。

zap支持多个sink,可以同时在多个文件中写,比如可以在标准输出和文件中同时写

对应的源码在Config#openSinks

func open(paths []string) ([]zapcore.WriteSyncer, func(), error) {writers := make([]zapcore.WriteSyncer, 0, len(paths))closers := make([]io.Closer, 0, len(paths))close := func() {for _, c := range closers {c.Close()}}var openErr errorfor _, path := range paths {// 重点是在这里,通过不同的path来创建不同的sinksink, err := _sinkRegistry.newSink(path)if err != nil {openErr = multierr.Append(openErr, fmt.Errorf("open sink %q: %w", path, err))continue}writers = append(writers, sink)closers = append(closers, sink)}if openErr != nil {close()return nil, nil, openErr}return writers, close, nil
}

WriteSyncer是zap自己封装的writer接口,增加Sync方法

上面在通过path从_sinkRegistry中获取不同的sink,这用的是简单工厂模式(其实也可以是策略,策略偏重的是行为,简单工厂偏重的是创建,这里用简单工厂比较合适)

sink源码:https://github.com/uber-go/zap/blob/master/sink.go

sink的接口定义

type Sink interface {zapcore.WriteSyncerio.Closer
}

创建sink

func (sr *sinkRegistry) newSink(rawURL string) (Sink, error) {// URL parsing doesn't work well for Windows paths such as `c:\log.txt`, as scheme is set to// the drive, and path is unset unless `c:/log.txt` is used.// To avoid Windows-specific URL handling, we instead check IsAbs to open as a file.// filepath.IsAbs is OS-specific, so IsAbs('c:/log.txt') is false outside of Windows.if filepath.IsAbs(rawURL) {return sr.newFileSinkFromPath(rawURL)}// 解析path,通过不同的scheme来做u, err := url.Parse(rawURL)if err != nil {return nil, fmt.Errorf("can't parse %q as a URL: %v", rawURL, err)}if u.Scheme == "" {u.Scheme = schemeFile}sr.mu.Lock()// 简单工厂模式,factory, ok := sr.factories[u.Scheme]sr.mu.Unlock()if !ok {return nil, &errSinkNotFound{u.Scheme}}return factory(u)
}func (sr *sinkRegistry) newFileSinkFromPath(path string) (Sink, error) {// 处理标准输入和输出,switch path {case "stdout":return nopCloserSink{os.Stdout}, nilcase "stderr":return nopCloserSink{os.Stderr}, nil}// 创建文件,这里返回的是File对象,File对象也实现了Sink接口,这就是go中的接口的正交性return sr.openFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
}

zap支持多个sink,为了方便调用,需要建多个sink聚合为一个对象,之后对这个对象操作。这用到了装饰器模式。

可以通过不同的scheme来做拓展,比如我们可以自己实现sink接口,直接让日志文件写在mq中去。(这个操作不难,规定scheme,比如rocketmq://top,然后实现sink,做mq的写操作就好了)

具体实现是:让此对象也实现相同接口,在对象中用列表保存sink,所有的操作遍历就好了

在这里插入图片描述

日志级别如何处理

跳出zap。日志级别简单来说输出日志的时候if以下就行了。if的位置有两种

  1. 在写日志的时候,自己手动先if以下

    if (logger.isDebug()){logger.debug() // 
    }
    
  2. 将if操作写在日志方法中

    logger.Debug()
    

zap采用的是第二种。

这其实没什么可说的,就是将日志级别放在logger对象中,打印日志的时候判断一下就好了。我们来看一下他的设计

接口:

type leveledEnabler interface {LevelEnablerLevel() Level
}type LevelEnabler interface {Enabled(Level) bool
}

有两个实现的结构体

  1. Level

    支持的level

    type Level int8const (// DebugLevel logs are typically voluminous, and are usually disabled in// production.DebugLevel Level = iota - 1// InfoLevel is the default logging priority.InfoLevel// WarnLevel logs are more important than Info, but don't need individual// human review.WarnLevel// ErrorLevel logs are high-priority. If an application is running smoothly,// it shouldn't generate any error-level logs.ErrorLevel// DPanicLevel logs are particularly important errors. In development the// logger panics after writing the message.DPanicLevel// PanicLevel logs a message, then panics.PanicLevel// FatalLevel logs a message, then calls os.Exit(1).FatalLevel
    )
    
  2. AtomicLevel

    type AtomicLevel struct {l *atomic.Int32 
    }
    

    对level包装了一层,提供了原子操作。为了并发安全

为了上面这些,只需要在logger输出的时候调用他的enabled就好了。

如何提高速度?

减少对象的分配,尽量复用对象。在go中本身就提供了sync.pool。只需要将整个操作期间的重对象池化,重用对象。

那么问题来了,哪些对象需要放在里面呢?

  1. buffer

    byte数组,大小为1kb,承担一些转换的操作,比如调用站格式化的时候需要用到string和byte,利用buffer来做,减少内存的分配

  2. jsonEncoder

    json的编解码器,将日志格式为json,这里面有两个buffer

    • jsonEncoder自己的buffer
    • jsonEncoder在编解码过程中用的到byte数组
  3. stacktrace

  4. entry

    一个日志条目,代表一条日志。此对象并不会占用太多的内存,并且也会及时的gc掉,但这里复用的目的是在于 日志在一个日志系统中会出现很多很多的。复用此对象会减少对象的分配。从而提高速度

    type Entry struct {Level      LevelTime       time.TimeLoggerName stringMessage    stringCaller     EntryCallerStack      string
    }
    

日志如何编码?

支持两种编码方式

  • jsonEncoder
  • consoleEncoder

zap提供了Encoder接口

type ObjectEncoder interface {// Logging-specific marshalers.AddArray(key string, marshaler ArrayMarshaler) errorAddObject(key string, marshaler ObjectMarshaler) error// Built-in types.AddBinary(key string, value []byte)     // for arbitrary bytesAddByteString(key string, value []byte) // for UTF-8 encoded bytesAddBool(key string, value bool)AddComplex128(key string, value complex128)AddComplex64(key string, value complex64)AddDuration(key string, value time.Duration)AddFloat64(key string, value float64)AddFloat32(key string, value float32)AddInt(key string, value int)AddInt64(key string, value int64)AddInt32(key string, value int32)AddInt16(key string, value int16)AddInt8(key string, value int8)AddString(key, value string)AddTime(key string, value time.Time)AddUint(key string, value uint)AddUint64(key string, value uint64)AddUint32(key string, value uint32)AddUint16(key string, value uint16)AddUint8(key string, value uint8)AddUintptr(key string, value uintptr)// AddReflected uses reflection to serialize arbitrary objects, so it can be// slow and allocation-heavy.AddReflected(key string, value interface{}) error// OpenNamespace opens an isolated namespace where all subsequent fields will// be added. Applications can use namespaces to prevent key collisions when// injecting loggers into sub-components or third-party libraries.OpenNamespace(key string)
}type Encoder interface {ObjectEncoder// Clone copies the encoder, ensuring that adding fields to the copy doesn't// affect the original.Clone() Encoder// EncodeEntry encodes an entry and fields, along with any accumulated// context, into a byte buffer and returns it. Any fields that are empty,// including fields on the `Entry` type, should be omitted.EncodeEntry(Entry, []Field) (*buffer.Buffer, error)
}

这些Encoder,会将一个Entry和Field数组变为一个buffer返回。JsonEncoder和ConsoleEncoder的区别是格式不同。

Zap会将我们日志中的参数推测为zap种定义的类型,然后不同的格式,调用不同的Add方法,添加到Encoder中去,从而Encoder可以执行自己的操作。

源码:

https://github.com/uber-go/zap/blob/master/zapcore/json_encoder.go

https://github.com/uber-go/zap/blob/master/zapcore/console_encoder.go

如果做到线程安全?

加锁,和正常的写一样,实现WriteSyncer接口,用代理模式给Write方法增加了同步的能力

type lockedWriteSyncer struct {sync.Mutexws WriteSyncer
}

如何拓展

用的是简单工厂模式

实现:

func RegisterEncoder(name string, constructor func(zapcore.EncoderConfig) (zapcore.Encoder, error)) error {_encoderMutex.Lock()defer _encoderMutex.Unlock()if name == "" {return errNoEncoderNameSpecified}if _, ok := _encoderNameToConstructor[name]; ok {return fmt.Errorf("encoder already registered for name %q", name)}_encoderNameToConstructor[name] = constructorreturn nil
}

有哪些拓展点?

  • 编码器
  • sink

dubbo-go中zap的使用

  1. 利用私有包变量

    本质还是用的是zap。但需要创建一个Sugar,需要作为变量放在包里面,然后用包提供的方法来访问,然后日志包提供对应的方法来打印日志

    在这里插入图片描述

  2. 提供logger接口

    这利用go里面的接口正交性

    type Logger interface {Info(args ...interface{})Warn(args ...interface{})Error(args ...interface{})Debug(args ...interface{})Fatal(args ...interface{})Infof(fmt string, args ...interface{})Warnf(fmt string, args ...interface{})Errorf(fmt string, args ...interface{})Debugf(fmt string, args ...interface{})Fatalf(fmt string, args ...interface{})
    }
    

    这是抽象出来的接口,zap的Sugar中有这些方法,就认为已经实现了此方法。

  3. 提供log的配置

    创建config对象,将zap中日志能配置的属性都放在自己的config中。

到此,分析结束了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/82981.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

源码编译Qt 5.15.9+msvc2019

官方文档里给出了详细步骤: Building Qt Sources Building Qt 5 from Git (Wiki) 注:本文基于windows11vs2019x64qt5.15.9,不编译Qt WebEngine 归纳总结如下: 准备阶段 Qt for Windows - Requirements 安装python,…

[npm]package.json文件

[npm]package.json文件 生成 package.jsonpackage.json 必须属性nameversion 描述信息descriptionkeywordsauthorcontributorshomepagerepositorybugs 依赖配置dependenciesdevDependenciespeerDependenciesoptionalDependenciesbundledDependenciesengines 脚本配置scriptscon…

无涯教程-JavaScript - PI函数

描述 PI函数返回数字3.14159265358979,数学常数pi,精确到15位数字。 语法 PI ()争论 PI函数语法没有参数。 适用性 Excel 2007,Excel 2010,Excel 2013,Excel 2016 Example JavaScript 中的 PI函数 - 无涯教程网无涯教程网提供描述PI函数返回数字3.14159265358979,数学常…

半导体划片机工艺应用

半导体划片工艺是半导体制造过程中的重要步骤之一,主要用于将大尺寸的晶圆切割成小片,以便进行后续的制造和封装过程。以下是一些半导体划片工艺的应用: 晶圆划片:在半导体制造过程中,需要将大尺寸的晶圆切割成小片&am…

虚拟机(VMM)

一、虚拟机概念 虚拟机又名虚拟机管理程序、虚拟机监控程序、VMM 使用虚拟化技术,将一台物理机器虚拟化为多台虚拟机器,每台虚拟机器都可以独立一个操作系统。 传统的计算机,一台物理机器只能运行一个操作系统。 二、虚拟机的分类 第一类VMM&…

【Linux 服务器运维】定时任务 crontab 详解 | 文末送书

文章目录 前言一、crontab 介绍1.1 什么是 crontab1.2 crontab 命令工作流程1.3 Linux 定时任务分类 二、crontab 用法详解2.1 crond 服务安装2.2 crontab 文件内容分析2.3 crontab 命令用法2.3.1 查看定时任务列表2.3.2 编辑/创建定时任务2.3.3 删除定时任务2.3.4 其他 cronta…

微服务07-认识MQ+RabbitMQ入门

1.前言 了解同步调用和异步调用 1.1.同步调用 比如这里的支付服务,需要等待订单服务、短信服务…执行完毕才能执行,这样支付整个流程完毕需要500ms 然后如果订单、仓储等其中一个服务挂掉了,那么支付服务请求请求不了,挂掉的服…

typescrip接口 interface详解,以及ts实现多态

ts 接口 当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的 示例如下 当一个对象类型被多次使用时,可以看到,很明显代码有大量的冗余 let personTom: { name: string, age?: number, sayHi(name: string): void } {name: Tom,sayHi(n…

JVM G1垃圾回收器学习笔记

前言 最近在工作中遇到频繁FullGC且YoungGC时间有时特别长的情况,而自己对JVM的垃圾回收也是一知半解,因此需要对JVM做系统的了解,为快速解决工作中的问题,能有效分析GC日志和业务代码,先从G1垃圾回收器开始学习&…

【操作系统笔记】程序运行机制CPU指令集

内存地址 指针 / 引用 指针、引用本质上就是内存地址,有了内存地址就可以操作对应的内存数据了。 不同的数据类型 字节序 大端序(Big Endian):字节顺序从低地址到高地址顺序存储的字节序小端序(Little Endian&#…

Spring Boot2.7生成用于登录的图片验证码

先在 pom.xml 注入依赖 <dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version> </dependency>然后 需要在配置文件中声明一下DefaultKaptcha 的 bean对象 然后 我们…

76、SpringBoot 整合 MyBatis------使用 sqlSession 作为 Dao 组件(就是ssm那一套,在 xml 写sql)

就是 ssm 那套&#xff0c;在xml 上面写sql ★ 基于SqlSession来实现DAO组件的方式 - MyBatis提供的Starter会自动在Spring容器中配置SqlSession&#xff08;其实SqlSessionTemplate实现类&#xff09;、并将它注入其他组件&#xff08;如DAO组件&#xff09;- DAO组件可直接…

js-nginx配置字段适配前端服务

当我们有这样一个需求&#xff0c;前端同一套代码&#xff0c;但要根据一些特殊字段展示不同的内容&#xff0c;比如我们有一个场id&#xff0c;暂时这个场id放在前端&#xff0c;后端根据这个场id返回不同的数据&#xff0c;这里前端部署用的是yaml文件&#xff0c;平台是ranc…

如何使用Python构建OTP验证系统?

即使您的密码被盗&#xff0c;OTP验证系统也可以充当安全的关键要素。它让您无需记住密码&#xff0c;充当额外的安全层&#xff0c;并降低了网络钓鱼的风险。 不妨学习用Python建立一个OTP验证系统&#xff0c;它会向您的手机号码发送一个OTP&#xff0c;有效期只有两分钟&am…

linux 文件锁

建议锁,强制锁,记录锁的概念 建议锁&#xff1a; 如果某一个进程对一个文件持有一把锁之后&#xff0c;其他进程仍然可以直接对文件进行操作(open, read, write)而不会被系统禁止&#xff0c;即使这个进程没有持有锁。只是一种编程上的约定。建议锁只对遵守建议锁准则的进程生…

知识付费平台开发技术实践:构建数字学习的未来

引言 知识付费平台的兴起正在塑造着数字学习的未来。本文将介绍一些关键的技术实践&#xff0c;帮助开发者构建强大的知识付费平台&#xff0c;提供出色的数字学习体验。 1. 选择适当的技术栈 在开始知识付费平台的开发之前&#xff0c;首要任务是选择适当的技术栈。这包括…

TS中的数据类型

一、number类型 let c: number; c 10; c "hello"; // 不能复制string类型 二、string类型 let d: string; d "hello"; d 10; // 不能复制number类型 三、boolean类型 let e: boolean true; e false; e 10; // 不能赋值true和false以外的值 四…

嵌入式裸机轻量级架构探索总结

为什么会想着探索下嵌入式裸机的架构呢&#xff1f;是因为最近写了一个项目&#xff0c;项目开发接近尾声时&#xff0c;发现了一些问题&#xff1a; 1、项目中&#xff0c;驱动层和应用层掺杂在一起&#xff0c;虽然大部分是应用层调用驱动层&#xff0c;但是也存在驱动层调用…

笔试面试相关记录(4)

&#xff08;1&#xff09;实现防火墙的主流技术有哪些&#xff1f; 实施防火墙主要采用哪些技术 - 服务器 - 亿速云 (yisu.com) &#xff08;2&#xff09; char arr[][2] {a, b, c, d}; printf("%d", *(arr1)); 输出的是谁的地址&#xff1f;字符c 测试代码如下…

ThreeJS-3D教学一基础场景创建

Three.js 是一个开源的 JS 3D 图形库&#xff0c;用于创建和展示高性能、交互式的 3D 图形场景。它建立在 WebGL 技术之上&#xff0c;并提供了丰富的功能和工具&#xff0c;使开发者可以轻松地构建令人惊叹的 3D 可视化效果。 Three.js 提供了一套完整的工具和 API&#xff0…