Golang 结构化日志包 log/slog 详解(五):LogValuer 和函数包装

上一篇文章讲解了 log/slog 包中的分组、上下文和属性值类型,本文讲解下 LogValuer 和日志记录函数的正确包装方法。

slog.LogValuer

如果想改变或者自定义一个类型的日志记录行为,可以通过实现 slog.LogValuer 接口来实现,slog.LogValuer 接口的定义如下:

type LogValuer interface {
	LogValue() Value
}

定义了一个 LogValue 方法,返回一个 Value 类型的对象。如果一个类型实现了 LogValuer 接口,那么从它的 LogValue 方法返回的 Value 将被用于日志记录,可以用来控制该类型的值在日志中的显示方式。看个简单示例:

package main

import (
	"log/slog"
	"os"
)

type Token string

// 实现 slog.LogValuer 接口
// 避免泄露 token
func (Token) LogValue() slog.Value {
	return slog.StringValue("******")
}

func main() {
	t := Token("shhhh!")
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
	logger.Info("生成 token", "用户", "路多辛的博客", "token", t)
}

输出内容如下:

time=2023-10-15T15:06:58.253+08:00 level=INFO msg="生成 token" 用户=路多辛的博客 token=******

从安全角度看,密码或者token 等敏感信息是不能被记录在日志里面的,可以使用自定义的并且实现了 LogValue 的类型来避免这种情况产生。在这个例子中,当记录 token 日志时,token 会被转换为“******”后记录在日志里面。再看一个结合字段分组使用的示例:

package main

import (
	"log/slog"
)

type Name struct {
	First, Last string
}

func (n Name) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("first", n.First),
		slog.String("last", n.Last))
}

func main() {
	n := Name{"路多辛的博客", "路多辛的所思所想"}
	slog.Info("任务结束", "agent", n)
}

输出内容如下:

2023/10/15 15:06:09 INFO 任务结束 agent.first=路多辛的博客 agent.last=路多辛的所思所想

包装输出函数

日志记录函数使用调用堆栈上的反射来查找应用程序中日志记录调用的文件名和行号,这可能会导致包装 slog 的函数记录错误的的源信息。举个例子,如果在 mylog.go 中定义了一个日志记录函数 Infof,然后在 main.go 中调用了此函数,这种情况下,日志会将把源文件记录为 mylog.go 而不是 main.go。正确实现 Infof 函数的方式是将获取的源信息传递给 NewRecord 函数,示例代码如下:

package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"time"
)

func Infof(logger *slog.Logger, format string, args ...any) {
	if !logger.Enabled(context.Background(), slog.LevelInfo) {
		return
	}
	var pcs [1]uintptr
	runtime.Callers(2, pcs[:]) // skip [Callers, Infof]
	r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf(format, args...), pcs[0])
	_ = logger.Handler().Handle(context.Background(), r)
}

func main() {
	replace := func(groups []string, a slog.Attr) slog.Attr {
		// Remove time.
		if a.Key == slog.TimeKey && len(groups) == 0 {
			return slog.Attr{}
		}
		// Remove the directory from the source's filename.
		if a.Key == slog.SourceKey {
			source := a.Value.Any().(*slog.Source)
			source.File = filepath.Base(source.File)
		}
		return a
	}
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true, ReplaceAttr: replace}))
	Infof(logger, "message, %s", "formatted")
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路多辛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值