实习过程中功能需求分析:
重构 Log 模块,基于 logrus 搭建多日志客户端并接入飞书 WebHook。
Webhook
飞书允许用户在群组中创建一个自定义机器人,将其他渠道的消息通过Webhook的方式推送至该群组中。
Webhook顾名思义即网络钩子,也称为用户自定义 HTTP回调函数(user-defined HTTP callbacks),通常用于监听某些行为事件,当事件触发时会向用户指定的目标地址发送信息。
这段话对于大多数没有计算机基础知识的读者来说可能难以理解,但却无关紧要,因为我们只需要可以将其理解为「中间人」即可:
Logrus
go get github.com/sirupsen/logrus
package main
import (
"github.com/sirupsen/logrus"
)
func Log3() {
logrus.SetLevel(logrus.TraceLevel)
logrus.Trace("trace msg")
logrus.Debug("debug msg")
logrus.Info("info msg")
logrus.Warn("warn msg")
logrus.Error("error msg")
logrus.Fatal("fatal msg")
logrus.Panic("panic msg")
}
func main() {
Log3()
}
Panic:记录日志,然后panic。
Fatal:致命错误,出现错误时程序无法正常运转。输出日志后,程序退出;
Error:错误日志,需要查看原因;
Warn:警告信息,提醒程序员注意;
Info:关键操作,核心流程的日志;
Debug:一般程序中输出的调试信息;
Trace:很细粒度的信息,一般用不到;
另外,我们观察到输出中有三个关键信息,time、level和msg:
time:输出日志的时间;
level:日志级别;
msg:日志信息。
调用logrus.SetReportCaller(true)设置在输出日志中添加文件名和方法信息
logrus.SetReportCaller(true)
logrus.Info("info msg")
添加自定义字段
有时候需要在输出中添加一些字段,可以通过调用logrus.WithField
和logrus.WithFields
实现。logrus.WithFields
接受一个logrus.Fields
类型的参数,其底层实际上为map[string]interface{}
:
logrus.WithFields(logrus.Fields{
"name": "dj",
"age": 18,
}).Info("info msg")
requestLogger := logrus.WithFields(logrus.Fields{
"user_id": 10010,
"ip": "192.168.32.15",
})
requestLogger.Info("info msg")
requestLogger.Error("error msg")
实际上,WithFields
返回一个logrus.Entry
类型的值,它将logrus.Logger
和设置的logrus.Fields
保存下来。调用Entry
相关方法输出日志时,保存下来的logrus.Fields
也会随之输出。
重定向输出
package main
import (
"bytes"
"fmt"
log "github.com/sirupsen/logrus"
"io"
"os"
)
func Log4() {
// 创建一个 bytes.Buffer 用于内存中的日志存储
var buffer bytes.Buffer
// 打开或创建一个日志文件
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("Failed to log to file:", err)
return
}
defer file.Close()
// 创建一个 MultiWriter,它可以将日志同时写入多个 io.Writer
writers := []io.Writer{
os.Stdout, // 标准输出
&buffer, // 内存中的缓冲区
file, // 日志文件
}
// 使用 MultiWriter 创建一个新的 Writer
multiWriter := io.MultiWriter(writers...)
// 设置 logrus 的输出为 MultiWriter
log.SetOutput(multiWriter)
// 设置日志格式为 JSON 格式
log.SetFormatter(&log.JSONFormatter{})
// 设置日志级别
log.SetLevel(log.DebugLevel)
// 记录一些日志
log.Info("This is an info message")
log.Warn("This is a warning message")
log.Error("This is an error message")
// 从缓冲区读取日志并打印出来
fmt.Println("Buffered logs:")
fmt.Print(buffer.String())
}
bytes.Buffer
用于内存中的日志存储
bytes.Buffer 是 Go 语言标准库 bytes 包中的一个类型,它提供了一个可变大小的缓冲区,用于高效地读写字节数据。Buffer 实现了 io.Writer 和 io.Reader 接口,因此可以很方便地与需要这些接口的函数或方法一起使用。
package main
import (
"os"
"fmt"
"bytes"
)
func main() {
file, _ := os.Open("./test.txt")
buf := bytes.NewBufferString("Hello world ")
buf.ReadFrom(file) //将text.txt内容追加到缓冲器的尾部
fmt.Println(buf.String())
}
实际上,考虑到易用性,库一般会使用默认值创建一个对象,包最外层的方法一般都是操作这个默认对象。
// github.com/sirupsen/logrus/exported.go
var (
std = New()
)
func StandardLogger() *Logger {
return std
}
func SetOutput(out io.Writer) {
std.SetOutput(out)
}
func SetFormatter(formatter Formatter) {
std.SetFormatter(formatter)
}
func SetReportCaller(include bool) {
std.SetReportCaller(include)
}
func SetLevel(level Level) {
std.SetLevel(level)
}
func New() *Logger {
return &Logger{
Out: os.Stderr,
Formatter: new(TextFormatter),
Hooks: make(LevelHooks),
Level: InfoLevel,
ExitFunc: os.Exit,
ReportCaller: false,
}
}
创建自己的logger对象
创建自己的Logger
对象,使用方式与直接调用logrus
的方法类似
package main
import "github.com/sirupsen/logrus"
func main() {
log := logrus.New()
log.SetLevel(logrus.InfoLevel)
log.SetFormatter(&logrus.JSONFormatter{})
log.Info("info msg")
}
hook钩子
在 logrus
中,钩子(Hook)是一个非常强大的特性,它允许你在日志记录的生命周期中插入自定义逻辑。
可以为logrus
设置钩子,每条日志输出前都会执行钩子的特定方法
具体来说,钩子可以在日志记录之前或之后执行特定的操作,比如添加额外的字段、根据日志级别将日志输出到不同的目的地,发送错误到错误跟踪服务、将统计信息发送到 StatsD 或在致命错误时转储核心。等。
package logrus
// A hook to be fired when logging on the logging levels returned from
// `Levels()` on your implementation of the interface. Note that this is not
// fired in a goroutine or a channel with workers, you should handle such
// functionality yourself if your call is non-blocking and you don't wish for
// the logging calls for levels returned from `Levels()` to block.(q1)
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
// Internal type for storing the hooks on a logger instance.
type LevelHooks map[Level][]Hook
// Add a hook to an instance of logger. This is called with
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
func (hooks LevelHooks) Add(hook Hook) {
for _, level := range hook.Levels() {
hooks[level] = append(hooks[level], hook)
}
}
// Fire all the hooks for the passed level. Used by `entry.log` to fire
// appropriate hooks for a log entry.
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
for _, hook := range hooks[level] {
if err := hook.Fire(entry); err != nil {
return err
}
}
return nil
}
q1: 一个钩子(hook)会在日志记录时触发,触发条件是日志级别与你在接口实现中通过 Levels() 方法返回的日志级别相匹配。需要注意的是,这个钩子不会在 goroutine 或带有工作线程的通道中触发,如果你的调用是非阻塞的,并且你不希望由 Levels() 返回的日志级别的日志调用被阻塞,你应该自己处理这种功能。
这段代码描述了一个日志钩子的行为:它会在特定日志级别(由 Levels()
方法返回)的日志记录时触发,但不会在后台的 goroutine 或工作线程中自动执行。如果钩子的操作是非阻塞的,开发者需要自行处理并发或异步逻辑,以避免日志调用被阻塞。
Hook介绍
Hook: 接口,需实现Levels()和Fire()方法。
Levels()方法返回感兴趣的日志级别,输出其他日志时不会触发钩子。Fire是日志输出前调用的钩子方法。
LevelHooks: 这是一个类型别名,表示一个映射,键是日志级别,值是一个包含多个钩子的切片。通过这种方式,你可以为不同的日志级别注册不同的钩子。
实操
实现Hook接口的结构体,就可以实现钩子功能。
只需要在Fire
方法实现中,为entry.Data
添加字段就会输出到日志中。
package main
import (
"github.com/sirupsen/logrus"
)
type AppHook struct {
AppName string
}
func (h *AppHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (h *AppHook) Fire(entry *logrus.Entry) error {
entry.Data["app"] = h.AppName
return nil
}
func main() {
h := &AppHook{AppName: "awesome-web"}
logrus.AddHook(h)
logrus.Info("info msg")
}
logrus
的第三方 Hook 很多,我们可以使用一些 Hook 将日志发送到 redis/mongodb 等存储中:
-
mgorus:将日志发送到 mongodb;
-
logrus-redis-hook:将日志发送到 redis;
-
logrus-amqp:将日志发送到 ActiveMQ。
将日志发送到飞书机器人bot
https://www.feishu.cn/content/7271149634339422210
交互式消息卡片
在fire发送post请求给飞书webhook的api。