// 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(), } }