170 lines
4.2 KiB
Go
170 lines
4.2 KiB
Go
// 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(),
|
|
}
|
|
}
|