1 Star 0 Fork 0

天雨流芳 / go-micro-framework

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
otelzap_init.go 16.05 KB
一键复制 编辑 原始数据 按行查看 历史
天雨流芳 提交于 2024-03-20 16:18 . 通用的应用启动逻辑封装
package logger
import (
"context"
"errors"
"fmt"
"gitee.com/tylf2018/go-micro-framework/pkg/common/util/fileutil"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"net/url"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
)
const (
numAttr = 5
KeyRequestID string = "requestID"
KeyUsername string = "username"
KeyUserID string = "userid"
)
var (
logSeverityKey = attribute.Key("log.severity")
logMessageKey = attribute.Key("log.message")
logTemplateKey = attribute.Key("log.template")
_encoderMutex sync.RWMutex
)
// ZapLogger log的zap实现类,推荐调用NewZapLog方法创建
type ZapLogger struct {
*zap.Logger
sugarLogger *zap.SugaredLogger
withTraceID bool
minLevel zapcore.Level
errorStatusLevel zapcore.Level
caller bool
stackTrace bool
// 使用sugar打印日志,默认开启
withSugar bool
// extraFields contains a number of zap.Fields that are added to every log entry
extraFields []zap.Field
}
type ZapLoggerOption func(*ZapLogger)
func WithSugar(on bool) ZapLoggerOption {
return func(l *ZapLogger) {
l.withSugar = on
}
}
func WithCaller(on bool) ZapLoggerOption {
return func(l *ZapLogger) {
l.caller = on
}
}
func WithStackTrace(on bool) ZapLoggerOption {
return func(l *ZapLogger) {
l.stackTrace = on
}
}
func NewZapLog(opts *Options, zapOptions ...ZapLoggerOption) *ZapLogger {
if opts == nil {
opts = NewOptions() // 如果不传递 opts 则使用默认创建
} else {
if err := opts.Validate(); err != nil {
panic(err)
}
}
var zapLevel zapcore.Level
// 获取 opts.Level 等级 需要转化一下
if err := zapLevel.UnmarshalText([]byte(opts.Level)); err != nil {
zapLevel = zapcore.InfoLevel
}
// 日志文件路径如果是相对路径,需要转成绝对路径
replaceFileOutPutPath(opts)
encodeLevel := zapcore.CapitalLevelEncoder //将Level序列化为全大写字符串。例如, InfoLevel被序列化为INFO
// 当输出到本地路径时,禁止带颜色
if opts.Format == consoleFormat && opts.EnableColor {
encodeLevel = zapcore.CapitalColorLevelEncoder // 将Level序列化为全大写字符串并添加颜色。例如,InfoLevel被序列化为INFO,并被标记为蓝色。
}
encoderConfig := zapcore.EncoderConfig{ // 设置 编码器
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding, // 每行结束 换行
EncodeLevel: encodeLevel, // log 等级
EncodeTime: timeEncoder, // 时间编码器
EncodeDuration: milliSecondsDurationEncoder, // 把时间戳设置成毫秒
EncodeCaller: zapcore.ShortCallerEncoder, // 行调用编码器
}
loggerConfig := &zap.Config{
Level: zap.NewAtomicLevelAt(zapLevel), // logger 启动的最小级别
Development: opts.Development, // 是否是开发模式
DisableCaller: opts.DisableCaller, // 停止使用调用函数的文件名和行号注释日志, 在默认情况下 所有日志都有注释
DisableStacktrace: opts.DisableStacktrace, // 禁用自动捕获堆栈跟踪。默认情况下,在 dev 环境中捕获WarnLevel及以上级别的日志,在 pro 环境中捕获ErrorLevel及以上级别的日志
Sampling: &zap.SamplingConfig{ // 设置采样策略。nil 禁用采样。
Initial: 100,
Thereafter: 100,
},
Encoding: opts.Format, // 设置记录器的编码 有效值 console 和 json
EncoderConfig: encoderConfig,
OutputPaths: opts.OutputPaths, // 输出 默认输出到标准输出
ErrorOutputPaths: opts.ErrorOutputPaths, // 错误输出 默认输出到标准错误
}
// 如果是windows平台,需要注册windows的解析器
if runtime.GOOS == "windows" {
_ = zap.RegisterSink("winfile", newWinFileSink)
}
l, err := generateZapLogger(loggerConfig, opts, zap.AddStacktrace(zapcore.PanicLevel), zap.AddCallerSkip(2))
if err != nil {
panic(err)
}
logger := &ZapLogger{
//我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层:
Logger: l.WithOptions(zap.AddCallerSkip(1)),
sugarLogger: l.Sugar(),
minLevel: zapLevel,
errorStatusLevel: zap.ErrorLevel,
withTraceID: true, // 打印log时 额外打印 trace_id 信息
withSugar: true,
}
if len(zapOptions) != 0 {
for _, opt := range zapOptions {
opt(logger)
}
}
/*
当应用程序中同时使用了 log 模块和 zap 日志库时,如果不对两者进行整合,就可能会出现一些问题:
1. 日志信息不统一:log 模块的日志输出格式和 zap 日志库的输出格式可能不一致,导致在分析和处理日志时比较麻烦。
2. 日志信息遗漏:如果不将 log 模块的日志信息也输入到 zap 日志库中,就可能会出现部分日志信息遗漏的情况。
3. 日志信息错乱:如果将 log 模块的日志信息和 zap 日志库的信息分别输出到不同的文件或控制台中,就可能会出现信息错乱或混杂的情况。
因此,我们可以使用 zap.RedirectStdLog 方法将 log 模块的日志信息重定向到 zap 日志库中,从而避免上述问题。
这个方法的作用是将 log 模块的输出重定向到 zap 日志库中,从而使得两者的日志信息都能够被 zap 日志库捕获和处理,同时也能够保证日志信息的统一、完整和正确输出。
*/
zap.RedirectStdLog(l)
return logger
}
func replaceFileOutPutPath(opts *Options) {
if len(opts.OutputPaths) > 0 {
opts.OutputPaths = replaceToRedirectPath(opts.OutputPaths)
}
if len(opts.ErrorOutputPaths) > 0 {
opts.ErrorOutputPaths = replaceToRedirectPath(opts.ErrorOutputPaths)
}
}
func replaceToRedirectPath(paths []string) []string {
if len(paths) == 0 {
return []string{}
}
redirectPath := make([]string, 0, len(paths))
for _, path := range paths {
if strings.HasPrefix(path, "./") {
path = strings.Replace(path, "./", fileutil.GetCurrentDirectory(), 1)
if runtime.GOOS == "windows" {
path = "winfile:///" + path
}
}
redirectPath = append(redirectPath, path)
}
return redirectPath
}
func generateZapLogger(loggerConfig *zap.Config, opts *Options, zapOpts ...zap.Option) (*zap.Logger, error) {
// 不使用文件分割,直接用原来的即可
if !opts.EnableFileSegment {
return loggerConfig.Build(zapOpts...)
}
// 如果不适用文件分割和输出文件中没有文件路径,只有标准输出,则只需要创建默认的即可
stdOutPaths, fileOutPaths := segmentStdAndFileOutPut(opts.OutputPaths)
if len(stdOutPaths) == 0 && len(fileOutPaths) == 0 {
return nil, fmt.Errorf("output path is empty")
}
errorStdPath, errFilePaths := segmentStdAndFileOutPut(opts.ErrorOutputPaths)
if len(errorStdPath) == 0 && len(errFilePaths) == 0 {
return nil, fmt.Errorf("error output path is empty")
}
if len(fileOutPaths) == 0 && len(errFilePaths) == 0 {
return loggerConfig.Build(zapOpts...)
}
// 如果有输出文件路径,则需要创建多个输出文件
outWriteSync, err := generateStdAndFileWriteSyncer(stdOutPaths, fileOutPaths, opts)
if err != nil {
return nil, err
}
errorWriteSync, err := generateStdAndFileWriteSyncer(errorStdPath, errFilePaths, opts)
if err != nil {
return nil, err
}
// 生成encoder对象
encoder, err := newEncoder(loggerConfig.Encoding, loggerConfig.EncoderConfig)
if err != nil {
return nil, err
}
if loggerConfig.Level == (zap.AtomicLevel{}) {
return nil, errors.New("missing Level")
}
log := zap.New(
zapcore.NewCore(encoder, outWriteSync, loggerConfig.Level),
buildErrOptions(errorWriteSync, loggerConfig)...,
)
if len(zapOpts) > 0 {
log = log.WithOptions(zapOpts...)
}
return log, nil
}
func buildErrOptions(errSink zapcore.WriteSyncer, cfg *zap.Config) []zap.Option {
opts := []zap.Option{zap.ErrorOutput(errSink)}
if cfg.Development {
opts = append(opts, zap.Development())
}
if !cfg.DisableCaller {
opts = append(opts, zap.AddCaller())
}
stackLevel := zap.ErrorLevel
if cfg.Development {
stackLevel = zap.WarnLevel
}
if !cfg.DisableStacktrace {
opts = append(opts, zap.AddStacktrace(stackLevel))
}
if scfg := cfg.Sampling; scfg != nil {
opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core {
var samplerOpts []zapcore.SamplerOption
if scfg.Hook != nil {
samplerOpts = append(samplerOpts, zapcore.SamplerHook(scfg.Hook))
}
return zapcore.NewSamplerWithOptions(
core,
time.Second,
cfg.Sampling.Initial,
cfg.Sampling.Thereafter,
samplerOpts...,
)
}))
}
if len(cfg.InitialFields) > 0 {
fs := make([]zap.Field, 0, len(cfg.InitialFields))
keys := make([]string, 0, len(cfg.InitialFields))
for k := range cfg.InitialFields {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fs = append(fs, zap.Any(k, cfg.InitialFields[k]))
}
opts = append(opts, zap.Fields(fs...))
}
return opts
}
func newEncoder(name string, encoderConfig zapcore.EncoderConfig) (zapcore.Encoder, error) {
if name == "" {
return nil, fmt.Errorf("encoder name is empty")
}
switch strings.ToLower(name) {
case "console":
return zapcore.NewConsoleEncoder(encoderConfig), nil
case "json":
return zapcore.NewJSONEncoder(encoderConfig), nil
default:
return nil, fmt.Errorf("not support encoder name :%v , must in [%v,%v]", name, "console", "json")
}
}
func generateStdAndFileWriteSyncer(stdOutPaths, fileOutPaths []string, opts *Options) (zapcore.WriteSyncer, error) {
stdWriteSyncer, err := generateStdWriteSyncer(stdOutPaths)
if err != nil {
return nil, err
}
fileSegmentWriteSyncer, err := generateFileSegmentWriteSyncer(fileOutPaths, opts)
if err != nil {
return nil, err
}
if stdWriteSyncer == nil && fileSegmentWriteSyncer == nil {
return nil, fmt.Errorf("generate out writeSyncer empty")
}
if stdWriteSyncer == nil {
return fileSegmentWriteSyncer, nil
}
if fileSegmentWriteSyncer == nil {
return stdWriteSyncer, nil
}
return zap.CombineWriteSyncers(stdWriteSyncer, fileSegmentWriteSyncer), nil
}
func generateFileSegmentWriteSyncer(fileOutPaths []string, opts *Options) (zapcore.WriteSyncer, error) {
if len(fileOutPaths) == 0 {
return nil, nil
}
fileSyncers := make([]zapcore.WriteSyncer, 0)
segmentConfig := opts.FileSegment
for _, path := range fileOutPaths {
// 因为用的是lumber,这里要将winfile去掉
if strings.HasPrefix(path, "winfile:///") {
path = strings.Replace(path, "winfile:///", "", 1)
}
lumberJackLogger := &lumberjack.Logger{
Filename: path, // 文件位置
MaxSize: segmentConfig.MaxSize, // 进行切割之前,日志文件的最大大小(MB为单位)
MaxAge: segmentConfig.MaxAge, // 保留旧文件的最大天数
MaxBackups: segmentConfig.MaxBackups, // 保留旧文件的最大个数
Compress: false, // 是否压缩/归档旧文件
}
fileSyncers = append(fileSyncers, zapcore.AddSync(lumberJackLogger))
}
return zap.CombineWriteSyncers(fileSyncers...), nil
}
func generateStdWriteSyncer(stdOutPaths []string) (zapcore.WriteSyncer, error) {
if len(stdOutPaths) == 0 {
return nil, nil
}
writer, closeOut, err := zap.Open(stdoutPath)
if err != nil {
closeOut()
return nil, err
}
return writer, nil
}
// 从输出中拆分出std的输出和文件的输出
func segmentStdAndFileOutPut(outputs []string) (stdOutputs []string, fileOutputs []string) {
if len(outputs) == 0 {
return
}
for _, output := range outputs {
// 如果是文件路径,是区分大小写的,不能变,要用临时变量
temp := strings.Trim(output, " ")
if temp == "" {
continue
}
temp = strings.ToLower(temp)
if temp == stdoutPath || temp == stderrPath {
stdOutputs = append(stdOutputs, temp)
} else {
fileOutputs = append(fileOutputs, output)
}
}
return
}
// WithOptions clones the current Logger, applies the supplied Options,
// and returns the resulting Logger. It's safe to use concurrently.
func (l *ZapLogger) WithOptions(opts ...zap.Option) *ZapLogger {
extraFields := []zap.Field{}
// zap.New side effect is extracting fields from .WithOptions(zap.Fields(...))
zap.New(&fieldExtractorCore{extraFields: &extraFields}, opts...)
clone := *l
clone.Logger = l.Logger.WithOptions(opts...)
clone.extraFields = append(clone.extraFields, extraFields...)
return &clone
}
func (l *ZapLogger) Flush() {
_ = l.Logger.Sync()
_ = l.sugarLogger.Sync()
}
func (l *ZapLogger) logFields(ctx context.Context, lvl zapcore.Level, msg string) []zapcore.Field {
var fields []zapcore.Field = make([]zapcore.Field, 0)
if lvl < l.minLevel || ctx == nil {
return fields
}
switch ctx.(type) {
case *gin.Context: // gin 的 context 与 普通的context 不太一样 gin 的内容存储在 ctx.Request.Context
if requestID := ctx.Value(KeyRequestID); requestID != nil {
if value, ok := requestID.(string); ok && value != "" {
fields = append(fields, zap.String(KeyRequestID, value))
}
}
if username := ctx.Value(KeyUsername); username != nil {
if value, ok := username.(string); ok && value != "" {
fields = append(fields, zap.String(KeyUsername, value))
}
}
ctx = ctx.(*gin.Context).Request.Context()
}
// 从 context 中获取 span
span := trace.SpanFromContext(ctx)
// 判断 span 存在 如果不存在 返回
if !span.IsRecording() {
return fields
}
attrs := make([]attribute.KeyValue, 0, numAttr+len(fields)+len(l.extraFields))
for _, f := range fields {
if f.Type == zapcore.NamespaceType {
// should this be a prefix?
continue
}
attrs = appendField(attrs, f)
}
for _, f := range l.extraFields {
if f.Type == zapcore.NamespaceType {
// should this be a prefix?
continue
}
attrs = appendField(attrs, f)
}
// 设置链路追踪相关代码
l.log(span, lvl, msg, attrs)
// 是否在log中打印 TraceID
if l.withTraceID {
traceID := span.SpanContext().TraceID().String()
fields = append(fields, zap.String("trace_id", traceID))
}
return fields
}
func (l *ZapLogger) log(span trace.Span, lvl zapcore.Level, msg string, attrs []attribute.KeyValue) {
attrs = append(attrs, logSeverityKey.String(levelString(lvl)))
attrs = append(attrs, logMessageKey.String(msg))
if l.caller {
// 这里 向上跳跃5层调用栈信息 返回最初始调用 此函数的地方
if fn, file, line, ok := runtimeCaller(5); ok {
if fn != "" {
attrs = append(attrs, semconv.CodeFunctionKey.String(fn))
}
if file != "" {
attrs = append(attrs, semconv.CodeFilepathKey.String(file))
attrs = append(attrs, semconv.CodeLineNumberKey.Int(line))
}
}
}
if l.stackTrace {
// 作用 返回调用栈信息
stackTrace := make([]byte, 2048)
// 获取当前 Goroutine 的调用栈信息,并将其写入到给定的缓冲区中
// buf 是一个缓冲区,用于存储调用栈信息,all 参数用于控制是否输出所有 Goroutine 的调用栈信息
// 函数返回值是写入缓冲区的字节数。
n := runtime.Stack(stackTrace, false)
attrs = append(attrs, semconv.ExceptionStacktraceKey.String(string(stackTrace[0:n])))
}
span.AddEvent("log", trace.WithAttributes(attrs...))
// 如果 当前log等级 大于 error span设置error状态码
if lvl >= l.errorStatusLevel {
span.SetStatus(codes.Error, msg)
}
}
func runtimeCaller(skip int) (fn, file string, line int, ok bool) {
rpc := make([]uintptr, 1)
// 这里 +1 的原因是 当前函数也算一层栈信息
n := runtime.Callers(skip+1, rpc[:])
if n < 1 {
return
}
frame, _ := runtime.CallersFrames(rpc).Next()
return frame.Function, frame.File, frame.Line, frame.PC != 0
}
func newWinFileSink(u *url.URL) (zap.Sink, error) {
// Remove leading slash left by url.Parse()
var name string
if u.Path != "" {
name = u.Path[1:]
} else if u.Opaque != "" {
name = u.Opaque[1:]
} else {
return nil, errors.New("path error")
}
return os.OpenFile(name, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
}
1
https://gitee.com/tylf2018/go-micro-framework.git
git@gitee.com:tylf2018/go-micro-framework.git
tylf2018
go-micro-framework
go-micro-framework
a23f37e8bd2b

搜索帮助