modelRT/logger/logger.go

170 lines
4.2 KiB
Go
Raw Normal View History

// Package logger define log struct of modelRT project
package logger
import (
"context"
"path"
"runtime"
"strings"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Logger is the interface returned by New for structured, trace-aware logging.
type Logger interface {
Debug(msg string, kv ...any)
Info(msg string, kv ...any)
Warn(msg string, kv ...any)
Error(msg string, kv ...any)
}
type logger struct {
ctx context.Context
_logger *zap.Logger
}
func (l *logger) Debug(msg string, kv ...any) {
l.log(zapcore.DebugLevel, msg, kv...)
}
func (l *logger) Info(msg string, kv ...any) {
l.log(zapcore.InfoLevel, msg, kv...)
}
func (l *logger) Warn(msg string, kv ...any) {
l.log(zapcore.WarnLevel, msg, kv...)
}
func (l *logger) Error(msg string, kv ...any) {
l.log(zapcore.ErrorLevel, msg, kv...)
}
func (l *logger) log(lvl zapcore.Level, msg string, kv ...any) {
caller := resolveLoggerCaller(0)
fields := makeLogFieldsWithCaller(l.ctx, caller, kv...)
ce := l._logger.Check(lvl, msg)
if ce == nil {
return
}
setCheckedEntryCaller(ce, caller)
ce.Write(fields...)
}
func makeLogFields(ctx context.Context, kv ...any) []zap.Field {
return makeLogFieldsSkip(ctx, 0, kv...)
}
func makeLogFieldsSkip(ctx context.Context, extraSkip int, kv ...any) []zap.Field {
return makeLogFieldsWithCaller(ctx, resolveLoggerCaller(extraSkip), kv...)
}
func makeLogFieldsWithCaller(ctx context.Context, caller loggerCaller, kv ...any) []zap.Field {
if len(kv)%2 != 0 {
kv = append(kv, "unknown")
}
spanCtx := trace.SpanFromContext(ctx).SpanContext()
traceID := spanCtx.TraceID().String()
spanID := spanCtx.SpanID().String()
kv = append(kv, "traceID", traceID, "spanID", spanID)
kv = append(kv, "func", caller.funcName, "file", caller.shortFile, "line", caller.line)
fields := make([]zap.Field, 0, len(kv)/2)
for i := 0; i < len(kv); i += 2 {
key := kv[i].(string)
value := kv[i+1]
switch v := value.(type) {
case string:
fields = append(fields, zap.String(key, v))
case int:
fields = append(fields, zap.Int(key, v))
case int64:
fields = append(fields, zap.Int64(key, v))
case float32:
fields = append(fields, zap.Float32(key, v))
case float64:
fields = append(fields, zap.Float64(key, v))
case bool:
fields = append(fields, zap.Bool(key, v))
case error:
fields = append(fields, zap.Error(v))
default:
fields = append(fields, zap.Any(key, v))
}
}
return fields
}
// getLoggerCallerInfo returns caller info at a fixed depth for the standard facade call chain.
func getLoggerCallerInfo() (funcName, file string, line int) {
return getLoggerCallerInfoSkip(0)
}
// getLoggerCallerInfoSkip returns caller info with additional skip frames beyond the standard depth.
func getLoggerCallerInfoSkip(extraSkip int) (funcName, file string, line int) {
caller := resolveLoggerCaller(extraSkip)
return caller.funcName, caller.shortFile, caller.line
}
type loggerCaller struct {
pc uintptr
funcName string
fullFile string
shortFile string
line int
}
func resolveLoggerCaller(extraSkip int) loggerCaller {
pcs := make([]uintptr, 32)
count := runtime.Callers(2, pcs)
frames := runtime.CallersFrames(pcs[:count])
for {
frame, more := frames.Next()
if !isLoggerInfrastructureFrame(frame.Function) {
if extraSkip > 0 {
extraSkip--
} else {
return loggerCaller{
pc: frame.PC,
funcName: frame.Function,
fullFile: frame.File,
shortFile: path.Base(frame.File),
line: frame.Line,
}
}
}
if !more {
return loggerCaller{}
}
}
}
func isLoggerInfrastructureFrame(function string) bool {
return strings.HasPrefix(function, "modelRT/logger.") ||
strings.HasPrefix(function, "gorm.io/gorm")
}
func setCheckedEntryCaller(entry *zapcore.CheckedEntry, caller loggerCaller) {
if caller.pc == 0 {
return
}
entry.Entry.Caller = zapcore.EntryCaller{
Defined: true,
PC: caller.pc,
File: caller.fullFile,
Line: caller.line,
}
}
// New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted
// from the OTel span stored in ctx and included in every log entry.
func New(ctx context.Context) Logger {
return &logger{
ctx: ctx,
_logger: GetLoggerInstance(),
}
}