ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LinuxKit 中的 Logrus 实战:Go 结构化日志库的完整使用指南

LinuxKit 中的 Logrus 实战:Go 结构化日志库的完整使用指南 操作系统云原生容器运行时【免费下载链接】linuxkitA toolkit for building secure, portable and lean operating systems for containers项目地址https://gitcode.com/gh_mirrors/li/linuxkit点击查看免费下载Logrus 是 Go 生态中最具影响力的结构化日志库之一它提供与标准库log完全兼容的 API同时引入字段Fields、格式化器Formatter、钩子Hook与多级别日志等能力广泛用于构建容器镜像、CLI 工具与系统级守护进程。本指南以 LinuxKit 仓库中实际 vendored 的 logrusv1.9.4见 go.mod为背景完整讲解其全部核心能力并结合 util/flags.go 的真实实现说明如何在 LinuxKit 这样的生产级 Go 工具链中落地使用。读完本文你将掌握从零接入、深度定制到测试验证的整套 Logrus 使用方案。Logrus 是什么与标准库 API 兼容的结构化日志库Logrus 是 Gogolang的结构化日志库目标是与标准库 logger 完全 API 兼容。这意味着你可以几乎无缝地把log替换为log github.com/sirupsen/logrus从而立即获得字段日志、格式化器与级别控制能力。需要特别说明的是Logrus 目前处于维护模式maintenance-mode官方不再引入新特性以避免破坏大量既有项目但它并未停止维护仍会持续进行安全修复、向后兼容的 bug 修复和性能优化。这一模式也直接催生了 Zerolog、Zap、Apex 等新一代日志库的繁荣——如果你需要更极致的性能与更轻量的依赖可以考虑这些替代方案但 Logrus 的 API 简洁性与生态成熟度仍使其成为大量项目包括 LinuxKit的首选。关于导入路径的大小写问题历史上存在同时以大写和小写导入 Logrus 的情况导致社区出现包路径冲突。官方最终统一标准为小写import github.com/sirupsen/logrus凡是使用其他大小写变体的包都应改为上述小写路径。LinuxKit 的 go.mod 中引用的正是github.com/sirupsen/logrus v1.9.4。快速上手从标准库 log 无缝迁移最简单的方式是直接使用包级导出的默认 loggerpackage main import github.com/sirupsen/logrus func main() { logrus.WithFields(logrus.Fields{ animal: walrus, }).Info(A walrus appears) }由于 API 与标准库完全兼容你可以全局替换导入import log github.com/sirupsen/logrus替换后原有的log.Printf、log.Fatal等调用照常工作同时获得 Logrus 的字段与格式化能力。从源码层面看这一兼容性由 logrus.go 中的两个接口保证StdLogger接口定义Print/Printf/Println、Fatal/Fatalf/Fatalln、Panic/Panicf/Panicln三组方法log.Logger、*Entry、*Logger均实现该接口源码中有编译期断言_ StdLogger log.Logger{}。FieldLogger接口在StdLogger之上扩展出WithField/WithFields/WithError以及各级别的f变体方法Entry与Logger均满足该接口。因此任何面向StdLogger或FieldLogger编写代码的库都能同时接受标准库 logger 与 logrus logger这正是兼容性的落点。全面定制格式化器、输出与级别包级 logger 可以在init()中一次性完成定制package main import ( os log github.com/sirupsen/logrus ) func init() { // Log as JSON instead of the default ASCII formatter. log.SetFormatter(log.JSONFormatter{}) // Output to stdout instead of the default stderr // Can be any io.Writer, see below for File example log.SetOutput(os.Stdout) // Only log the warning severity or above. log.SetLevel(log.WarnLevel) } func main() { log.WithFields(log.Fields{ animal: walrus, size: 10, }).Info(A group of walrus emerges from the ocean) log.WithFields(log.Fields{ omg: true, number: 122, }).Warn(The groups number increased tremendously!) log.WithFields(log.Fields{ omg: true, number: 100, }).Fatal(The ice breaks!) // A common pattern is to re-use fields between logging statements by re-using // the logrus.Entry returned from WithFields() contextLogger : log.WithFields(log.Fields{ common: this is a common field, other: I also should be logged always, }) contextLogger.Info(Ill be logged with common and other field) contextLogger.Info(Me too) }注意SetOutput接受任意io.Writer例如文件file, err : os.OpenFile(logrus.log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err nil { log.SetOutput(file) } else { log.Info(Failed to log to file, using default stderr) }TextFormatter默认格式化器的完整选项默认情况下当输出目标是 TTY 时Logrus 使用带颜色的文本输出否则输出与 logfmt 兼容的纯文本例如time2015-03-26T01:27:38-04:00 leveldebug msgStarted observing beach animalwalrus number8 time2015-03-26T01:27:38-04:00 levelinfo msgA group of walrus emerges from the ocean animalwalrus size10 time2015-03-26T01:27:38-04:00 levelwarning msgThe groups number increased tremendously! number122 omgtrue time2015-03-26T01:27:38-04:00 leveldebug msgTemperature changes temperature-4 time2015-03-26T01:27:38-04:00 levelpanic msgIts over 9000! animalorca size9009 time2015-03-26T01:27:38-04:00 levelfatal msgThe ice breaks! err{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic Its over 9000!} number100 omgtrue即使接在 TTY 上也可以强制关闭颜色并输出完整时间戳logrus.SetFormatter(logrus.TextFormatter{ DisableColors: true, FullTimestamp: true, })结合 text_formatter.go 的源码TextFormatter支持以下常用选项字段说明ForceColors即使没有 TTY 也强制输出颜色DisableColors即使有 TTY 也强制关闭颜色ForceQuote强制对所有值加引号DisableQuote禁止对所有值加引号优先级低于ForceQuoteEnvironmentOverrideColors根据CLICOLOR/CLICOLOR_FORCE环境变量覆盖颜色行为DisableTimestamp关闭时间戳适用于外层日志系统已加时间戳的场景FullTimestamp在 TTY 上输出完整时间戳而非运行相对时间TimestampFormat自定义时间格式语法与标准库time.Format一致DisableSorting关闭字段按键排序默认按键排序保证输出稳定SortingFunc自定义字段排序函数默认使用sort.StringsDisableLevelTruncation关闭级别文本截断默认颜色模式下级别截断为 4 个字符PadLevelText给级别文本补空格使各级别等宽、便于纵向扫描是DisableLevelTruncation的超集QuoteEmptyFields为空字段加引号FieldMap重命名默认字段键例如把time改为timestampCallerPrettyfier在启用ReportCaller时自定义函数名与文件名显示JSONFormatter面向日志聚合的输出通过logrus.SetFormatter(logrus.JSONFormatter{})即可切换到 JSON 输出便于 Logstash、Splunk 等工具直接解析{animal:walrus,level:info,msg:A group of walrus emerges from the ocean,size:10,time:2014-03-10 19:57:38.562264131 -0400 EDT} {level:warning,msg:The groups number increased tremendously!,number:122,omg:true,time:2014-03-10 19:57:38.562471297 -0400 EDT} {animal:walrus,level:info,msg:A giant walrus appears!,size:10,time:2014-03-10 19:57:38.562500591 -0400 EDT} {animal:walrus,level:info,msg:Tremendously sized cow enters the ocean.,size:9,time:2014-03-10 19:57:38.562527896 -0400 EDT} {level:fatal,msg:The ice breaks!,number:100,omg:true,time:2014-03-10 19:57:38.562543128 -0400 EDT}JSONFormatter同样支持TimestampFormat、FieldMap、DisableTimestamp、PrettyPrint等选项完整字段可查看其源码 json_formatter.go 与包文档。记录调用方方法名如果需要把调用方法名作为字段记录调用logrus.SetReportCaller(true)输出会附带method字段{animal:penguin,level:fatal,method:github.com/sirupsen/arcticcreatures.migrate,msg:a penguin swims by,time:2014-03-10 19:57:38.562543129 -0400 EDT}time2015-03-26T01:27:38-04:00 levelfatal methodgithub.com/sirupsen/arcticcreatures.migrate msga penguin swims by animalpenguin需要注意这会带来可测量的开销——在 Go 1.6/1.7 的测试中约为 20%40%具体取决于 Go 版本。可以在自己的环境中用基准测试验证go test -bench.*CallerTracing生产环境建议仅在排查问题时临时开启。创建自己的 Logger 实例多目的地日志当同一进程需要向多个位置输出、或需要不同配置的多套 logger 时可以实例化独立的Loggerpackage main import ( os github.com/sirupsen/logrus ) // Create a new instance of the logger. You can have any number of instances. var logger logrus.New() func main() { // The API for setting attributes is a little different than the package level // exported logger. See Godoc. logger.Out os.Stdout // You could set this to any io.Writer such as a file // file, err : os.OpenFile(logrus.log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) // if err nil { // logger.Out file // } else { // logger.Info(Failed to log to file, using default stderr) // } logger.WithFields(logrus.Fields{ animal: walrus, size: 10, }).Info(A group of walrus emerges from the ocean) }从 logger.go 源码可以看到Logger结构体的核心字段Out io.Writer日志输出目标默认os.Stderr可改为文件、Kafka 等任意io.WriterHooks LevelHooks按级别触发的事件钩子Formatter Formatter所有日志条目经过的格式化器默认TextFormatterReportCaller bool是否记录调用方信息默认关闭Level Level日志级别阈值默认InfoLevelmu MutexWrap并发写锁默认开启可关闭entryPool sync.Pool复用空Entry降低高频日志的分配开销ExitFunc exitFuncFatal后调用的退出函数默认os.ExitBufferPool BufferPool格式化用缓冲区池默认使用全局池。实例级 API 与包级略有差异例如实例用字段赋值logger.Out os.Stdout与logger.Formatter ...而包级用SetOutput、SetFormatter等方法。结构化字段Fields 与默认字段Logrus 鼓励通过字段进行结构化日志而非拼接一长串难以解析的错误消息。例如不要这样写logrus.Fatalf(Failed to send event %s to topic %s with key %d)而应输出更易检索的字段形式logrus.WithFields(logrus.Fields{ event: event, topic: topic, key: key, }).Fatal(Failed to send event)Fields本质上就是map[string]interface{}见 logrus.go 中的type Fields map[string]interface{}。WithFields调用是可选的但该 API 设计会迫使你以更有价值的方式思考日志内容——实践中往往只是给既有日志语句补一个字段就能节省数小时的排查时间。当然printf家族的Infof/Errorf等函数仍然可用只是它们通常提示你应该改用字段。默认字段Entry 复用某些字段希望始终出现在日志中例如请求上下文的request_id和user_ip。与其每行都写logger.WithFields(...)不如创建一个logrus.Entry四处传递requestLogger : logger.WithFields(logrus.Fields{request_id: request_id, user_ip: user_ip}) requestLogger.Info(something happened on that request) // will log request_id and user_ip requestLogger.Warn(something not great happened)Hooks按级别触发自定义行为Hook 允许在特定日志级别触发额外动作例如把Error/Fatal/Panic级别的错误上报给异常跟踪服务、把信息级别上报给 StatsD或同时把日志写到 syslog 等多处。Logrus 官方自带若干内置 hooksyslog、test 等也可以在init()中注册第三方或自定义 hookpackage main import ( log/syslog github.com/sirupsen/logrus airbrake gopkg.in/gemnasium/logrus-airbrake-hook.v2 logrus_syslog github.com/sirupsen/logrus/hooks/syslog ) func init() { // Use the Airbrake hook to report errors that have Error severity or above to // an exception tracker. You can create custom hooks, see the Hooks section. logrus.AddHook(airbrake.NewHook(123, xyz, production)) hook, err : logrus_syslog.NewSyslogHook(udp, localhost:514, syslog.LOG_INFO, ) if err ! nil { logrus.Error(Unable to connect to local syslog daemon) } else { logrus.AddHook(hook) } }syslog hook 除了远程 UDP 之外也支持本地 syslog 路径例如/dev/log、/var/run/syslog或/var/run/log。如果需要让全局日志与 syslog 日志使用不同级别可以参考 syslog hook 的文档说明。从 hooks.go 源码可知Logger.Hooks的类型为LevelHooksmap[Level][]Hook每个级别可挂多个 hookHook接口要求实现Levels() []Level与Fire(*Entry) error。因此自定义 hook 只需声明要监听的级别并实现Fire逻辑Logger 在写日志时会持有互斥锁调用所有匹配级别的 hook。七个日志级别与 Entries 自动字段Logrus 定义了七个级别从低到高为Trace、Debug、Info、Warning、Error、Fatal、Panic。源码 logrus.go 中的常量定义Level uint32数值自PanicLevel iota起意味着PanicLevel数值最小、TraceLevel数值最大SetLevel的阈值过滤即按此数值比较。logrus.Trace(Something very low level.) logrus.Debug(Useful debugging information.) logrus.Info(Something noteworthy happened!) logrus.Warn(You should probably take a look at this.) logrus.Error(Something failed but Im not quitting.) // Calls os.Exit(1) after logging logrus.Fatal(Bye.) // Calls panic() after logging logrus.Panic(Im bailing.)设置级别后只会记录该级别及以上的条目// Will log anything that is info or above (warn, error, fatal, panic). Default. logrus.SetLevel(logrus.InfoLevel)在调试或 verbose 环境中可设置logrus.Level logrus.DebugLevel。此外 logrus.go 提供ParseLevel(debug)这样的字符串解析函数支持warn/warning两种拼写并实现了encoding.TextUnmarshaler/Marshaler便于从配置文件反序列化级别。Entries 自动附加的字段除了用WithField/WithFields添加的字段外每个日志事件会自动附加三个字段time条目创建时的时间戳msg传给Info/Warn/Error/Fatal/Panic的日志消息在AddFields之后level日志级别例如info。这些字段与自定义字段一起存放在entry.DataFields类型中统一交给格式化器处理。环境适配Logrus 本身不感知环境Logrus 没有内置环境概念。如果希望 hook 和 formatter 只在特定环境生效需要自行处理。例如应用有一个表示环境的全局变量Environmentimport ( github.com/sirupsen/logrus ) func init() { // do something here to set environment depending on an environment variable // or command-line flag if Environment production { logrus.SetFormatter(logrus.JSONFormatter{}) } else { // The TextFormatter is default, you dont actually have to do this. logrus.SetFormatter(logrus.TextFormatter{}) } }这是 Logrus 的预期用法生产环境输出 JSON配合 Splunk、Logstash 等做日志聚合本地开发输出易读的文本。自定义 Formatter实现 Formatter 接口内置格式化器之外Logrus 还允许通过实现Formatter接口自定义输出。Formatter只需实现一个Format方法入参为*Entry其中entry.Data是Fields类型map[string]interface{}包含全部自定义字段与默认字段type MyJSONFormatter struct{} logrus.SetFormatter(new(MyJSONFormatter)) func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) { // Note this doesnt include Time, Level and Message which are available on // the Entry. Consult godoc on information about those fields or read the // source of the official loggers. serialized, err : json.Marshal(entry.Data) if err ! nil { return nil, fmt.Errorf(Failed to marshal fields to JSON, %w, err) } return append(serialized, \n), nil }注意entry.Data不包含Time、Level、Message这几个字段它们作为Entry的独立属性存在自定义格式化器可以按需读取参考官方格式化器源码例如 text_formatter.go 与 json_formatter.go。社区中还有一批第三方格式化器可供参考例如兼容 Kubernetes 与 GCE 日志的 Fluentd 格式化器符合 Graylog GELF 1.1 规范的 GELF 格式化器输出 Logstash 事件的 logstash 格式化器显示日志来源与替代布局的 prefixed 格式化器把字段转换为嵌套结构的 nested-logrus-formatter可输出文件名、行号与函数名的 powerful-logrus-formatter以及带人类可读标题的 caption-json-formatter 等。它们都是实现上述Formatter接口的范例选择第三方库时请以各库自身的文档为准。把 Logrus 当作 io.Writer 使用Logrus 可以转换为io.Writer。该 writer 是一个io.Pipe的写端需要调用方负责关闭w : logger.Writer() defer w.Close() srv : http.Server{ // create a stdlib log.Logger that writes to // logrus.Logger. ErrorLog: log.New(w, , 0), }写入该 writer 的每一行都会走常规的 formatter 与 hooks 流程条目的级别固定为info。这意味着可以轻松接管标准库 loggerlogger : logrus.New() logger.Formatter logrus.JSONFormatter{} // Use logrus for standard log output // Note that log here references stdlibs log // Not logrus imported under the name log. log.SetOutput(logger.Writer())此外Logger还提供WriterLevel(level)如 flags.go 中所用可生成按指定级别记录条目的 writer适合把第三方库的日志按级别汇入 logrus。底层实现位于 writer.go。日志轮转交给外部程序Logrus 本身不提供日志轮转功能。官方明确建议轮转应由外部程序如logrotate(8)完成由它们负责压缩与删除旧日志这不应成为应用级日志库的职责。如果你需要按大小或时间自动轮转可以在应用外部配置 logrotate或在Out处接入支持轮转的io.Writer封装第三方轮转库请以各自文档为准。测试支持内置 test hookLogrus 内置了用于断言日志输出行为的测试设施通过testhook 实现test.NewLocal/test.NewGlobal为现有 logger 添加testhook 的装饰器test.NewNullLogger只记录日志消息而不做任何输出的测试 logger。import ( testing github.com/sirupsen/logrus github.com/sirupsen/logrus/hooks/test github.com/stretchr/testify/assert ) func TestSomething(t *testing.T) { logger, hook : test.NewNullLogger() logger.Error(Helloerror) assert.Equal(t, 1, len(hook.Entries)) assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level) assert.Equal(t, Helloerror, hook.LastEntry().Message) hook.Reset() assert.Nil(t, hook.LastEntry()) }该 hook 会把每个日志事件追加到hook.Entries并提供LastEntry()与Reset()非常适合在单元测试中验证是否记录了某级别的某条消息。Fatal handlers 与线程安全注册退出处理器Logrus 支持注册一个或多个函数在任何fatal级别消息被记录时调用这些处理器会在 Logrus 执行os.Exit(1)之前执行。这很有用因为与可以通过defer recover()拦截的panic不同os.Exit(1)无法被拦截只能借助 exit handler 做优雅关闭handler : func() { // gracefully shut down something... } logrus.RegisterExitHandler(handler)线程安全与锁的取舍默认情况下Logger通过互斥锁保护并发写调用 hooks 与写日志时都会持锁。如果确定不需要锁可调用logger.SetNoLock()关闭。官方列出的不需要锁的典型场景没有注册 hooks或 hook 调用本身已是线程安全的写入logger.Out已是线程安全的例如logger.Out已被外部锁保护logger.Out是以O_APPEND打开的文件句柄且每次写入小于 4k此时多线程/多进程追加写入在常规文件系统上是原子的。从 logger.go 源码看该锁由MutexWrap内含sync.Mutex与disabled标志实现SetNoLock()实际调用mu.Disable()同时entryPool sync.Pool负责复用空Entry降低高频日志路径的分配压力。LinuxKit 中的真实落地util/flags.go 源码解析作为佐证LinuxKit 的linuxkit命令行工具在 util/flags.go 中展示了 logrus 在真实 CLI 工具中的典型用法值得借鉴。自定义 Formatter 以精简 Info 输出LinuxKit 定义了一个infoFormatter只对InfoLevel的条目输出纯消息加换行其余级别交给默认TextFormattervar defaultLogFormatter log.TextFormatter{} type infoFormatter struct{} func (f *infoFormatter) Format(entry *log.Entry) ([]byte, error) { if entry.Level log.InfoLevel { return append([]byte(entry.Message), \n), nil } return defaultLogFormatter.Format(entry) }按 CLI 标志动态切换级别与格式化器SetupLogging(quiet, verbose, verboseSet)依据-qquiet与-vverbose取值 03标志决定全局日志配置quiet或verbose 0SetLevel(log.ErrorLevel)只输出错误verbose 1切回标准TextFormatterSetLevel(log.InfoLevel)verbose 2SetLevel(log.DebugLevel)并把 go-containerregistry 的 Warn/Debug 日志通过StandardLogger().WriterLevel(...)接入 logrusverbose 3SetLevel(log.TraceLevel)输出最细粒度的追踪日志quiet与verbose 0同时设置会返回错误避免配置冲突。用 WriterLevel 统一第三方库日志通过log.StandardLogger().WriterLevel(log.WarnLevel)与WriterLevel(log.DebugLevel)创建io.Writer再喂给log.New(...)即可把 go-containerregistry 等第三方库的日志按级别汇入 logrus 的统一管道这正是前面Logger 作为 io.Writer一节在真实项目中的完整应用。LinuxKit 全仓库有大量模块以log github.com/sirupsen/logrus的方式导入例如build.go、moby/config.go、run_aws.go、cache/*.go等覆盖构建、镜像缓存、云平台运行等全部子命令是观察 logrus 在大型 Go CLI 工程中规模化使用的最佳范例。结语Logrus 以标准库兼容 结构化字段的简洁设计成为 Go 日志生态的基石之一从包级一行接入、Logger实例多目的地输出到TextFormatter/JSONFormatter的细粒度控制、hook 的事件扩展、io.Writer的第三方库接入以及 test hook 与 exit handler 的生产力保障构成了一套完整可用的日志方案。维护模式并不意味着不可用——LinuxKit 这类对稳定性要求极高的系统工具仍然将其作为核心日志依赖v1.9.4。理解 Logrus 的能力边界如不提供轮转、不感知环境再结合自定义 Formatter 与 WriterLevel 等进阶手法你就能在自己的 Go 项目中构建出既易读、又适合机器解析的日志体系。赞分享操作系统云原生容器运行时【免费下载链接】linuxkitA toolkit for building secure, portable and lean operating systems for containers项目地址https://gitcode.com/gh_mirrors/li/linuxkit点击查看免费下载相关推荐LinuxKit 中的 Go 结构化日志实践基于 Logrus 的完整指南LinuxKit 中的 Go 结构化日志实践基于 Logrus 的完整指南 导读 本文围绕 LinuxKit 仓库中随 pkg/metadata 与 pkg/操作系统云原生容器运行时Moby 项目中的 Logrus 使用指南Go 结构化日志库原理与实战Moby 项目中的 Logrus 使用指南Go 结构化日志库原理与实战 Logrus 是 Go 语言中应用最广泛的结构化日志库之一它保持与 Go 标准库 l云原生容器运行时虚拟化容器编排BuildKit 中的日志基石logrus 结构化日志库实战指南BuildKit 中的日志基石logrus 结构化日志库实战指南 logrus 是 Go 生态中应用最广泛的结构化日志库之一与标准库 log 完全 API构建工具云原生后端上一篇在 Termux 里跑 N_m3u8DL-RE手机下载与直播录制的 4 个实操场景下一篇如何强制调整Windows窗口大小WindowResizer完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表