Go modules with major version v2+ require the module path to include the version suffix. Without this, `go install` fails with: "invalid version: module contains a go.mod file, so module path must match major version" Updated go.mod and all internal imports to use codeberg.org/goern/forgejo-mcp/v2
279 lines
7 KiB
Go
279 lines
7 KiB
Go
package log
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"net/url"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/flag"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
var (
|
|
defaultLoggerOnce sync.Once
|
|
defaultLogger *zap.Logger
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const (
|
|
RequestIDKey contextKey = "request_id"
|
|
OperationKey contextKey = "operation"
|
|
)
|
|
|
|
func Default() *zap.Logger {
|
|
defaultLoggerOnce.Do(func() {
|
|
if defaultLogger == nil {
|
|
ec := zap.NewProductionEncoderConfig()
|
|
ec.EncodeTime = zapcore.TimeEncoderOfLayout(time.DateTime)
|
|
ec.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
|
|
|
var ws zapcore.WriteSyncer
|
|
var wss []zapcore.WriteSyncer
|
|
|
|
// wss = append(wss, zapcore.AddSync(os.Stdout))
|
|
wss = append(wss, zapcore.AddSync(os.Stderr))
|
|
ws = zapcore.NewMultiWriteSyncer(wss...)
|
|
|
|
enc := zapcore.NewConsoleEncoder(ec)
|
|
var level zapcore.Level
|
|
if flag.Debug {
|
|
level = zapcore.DebugLevel
|
|
} else {
|
|
level = zapcore.InfoLevel
|
|
}
|
|
core := zapcore.NewCore(enc, ws, level)
|
|
options := []zap.Option{
|
|
zap.AddStacktrace(zapcore.ErrorLevel),
|
|
zap.AddCaller(),
|
|
zap.AddCallerSkip(1),
|
|
}
|
|
defaultLogger = zap.New(core, options...)
|
|
}
|
|
})
|
|
|
|
return defaultLogger
|
|
}
|
|
|
|
func SetDefault(logger *zap.Logger) {
|
|
if logger != nil {
|
|
defaultLogger = logger
|
|
}
|
|
}
|
|
|
|
func Logger() *zap.Logger {
|
|
return defaultLogger
|
|
}
|
|
|
|
func Debug(msg string, fields ...zap.Field) {
|
|
Default().Debug(msg, fields...)
|
|
}
|
|
|
|
func Info(msg string, fields ...zap.Field) {
|
|
Default().Info(msg, fields...)
|
|
}
|
|
|
|
func Warn(msg string, fields ...zap.Field) {
|
|
Default().Warn(msg, fields...)
|
|
}
|
|
|
|
func Error(msg string, fields ...zap.Field) {
|
|
Default().Error(msg, fields...)
|
|
}
|
|
|
|
func Panic(msg string, fields ...zap.Field) {
|
|
Default().Panic(msg, fields...)
|
|
}
|
|
|
|
func Debugf(format string, args ...any) {
|
|
Default().Sugar().Debugf(format, args...)
|
|
}
|
|
|
|
func Infof(format string, args ...any) {
|
|
Default().Sugar().Infof(format, args...)
|
|
}
|
|
|
|
func Warnf(format string, args ...any) {
|
|
Default().Sugar().Warnf(format, args...)
|
|
}
|
|
|
|
func Errorf(format string, args ...any) {
|
|
Default().Sugar().Errorf(format, args...)
|
|
}
|
|
|
|
func Fatal(msg string, fields ...zap.Field) {
|
|
Default().Fatal(msg, fields...)
|
|
}
|
|
|
|
func Fatalf(format string, args ...any) {
|
|
Default().Sugar().Fatalf(format, args...)
|
|
}
|
|
|
|
// GenerateRequestID creates a new random request ID
|
|
func GenerateRequestID() string {
|
|
bytes := make([]byte, 8)
|
|
rand.Read(bytes)
|
|
return hex.EncodeToString(bytes)
|
|
}
|
|
|
|
// WithRequestID adds a request ID to the context
|
|
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
|
return context.WithValue(ctx, RequestIDKey, requestID)
|
|
}
|
|
|
|
// WithOperation adds an operation name to the context
|
|
func WithOperation(ctx context.Context, operation string) context.Context {
|
|
return context.WithValue(ctx, OperationKey, operation)
|
|
}
|
|
|
|
// GetContextFields extracts logging fields from context
|
|
func GetContextFields(ctx context.Context) []zap.Field {
|
|
var fields []zap.Field
|
|
|
|
if requestID, ok := ctx.Value(RequestIDKey).(string); ok && requestID != "" {
|
|
fields = append(fields, zap.String("request_id", requestID))
|
|
}
|
|
|
|
if operation, ok := ctx.Value(OperationKey).(string); ok && operation != "" {
|
|
fields = append(fields, zap.String("operation", operation))
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
// SanitizeURL removes sensitive information from URLs for logging
|
|
func SanitizeURL(rawURL string) string {
|
|
if rawURL == "" {
|
|
return ""
|
|
}
|
|
|
|
parsedURL, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return "[invalid-url]"
|
|
}
|
|
|
|
// Remove query parameters that might contain tokens
|
|
if parsedURL.RawQuery != "" {
|
|
parsedURL.RawQuery = "[redacted]"
|
|
}
|
|
|
|
// Remove user info (if any)
|
|
parsedURL.User = nil
|
|
|
|
return parsedURL.String()
|
|
}
|
|
|
|
// Context-aware logging functions
|
|
func DebugCtx(ctx context.Context, msg string, fields ...zap.Field) {
|
|
fields = append(GetContextFields(ctx), fields...)
|
|
Default().Debug(msg, fields...)
|
|
}
|
|
|
|
func InfoCtx(ctx context.Context, msg string, fields ...zap.Field) {
|
|
fields = append(GetContextFields(ctx), fields...)
|
|
Default().Info(msg, fields...)
|
|
}
|
|
|
|
func WarnCtx(ctx context.Context, msg string, fields ...zap.Field) {
|
|
fields = append(GetContextFields(ctx), fields...)
|
|
Default().Warn(msg, fields...)
|
|
}
|
|
|
|
func ErrorCtx(ctx context.Context, msg string, fields ...zap.Field) {
|
|
fields = append(GetContextFields(ctx), fields...)
|
|
Default().Error(msg, fields...)
|
|
}
|
|
|
|
func DebugfCtx(ctx context.Context, format string, args ...any) {
|
|
fields := GetContextFields(ctx)
|
|
Default().With(fields...).Sugar().Debugf(format, args...)
|
|
}
|
|
|
|
func InfofCtx(ctx context.Context, format string, args ...any) {
|
|
fields := GetContextFields(ctx)
|
|
Default().With(fields...).Sugar().Infof(format, args...)
|
|
}
|
|
|
|
func WarnfCtx(ctx context.Context, format string, args ...any) {
|
|
fields := GetContextFields(ctx)
|
|
Default().With(fields...).Sugar().Warnf(format, args...)
|
|
}
|
|
|
|
func ErrorfCtx(ctx context.Context, format string, args ...any) {
|
|
fields := GetContextFields(ctx)
|
|
Default().With(fields...).Sugar().Errorf(format, args...)
|
|
}
|
|
|
|
// Helper functions for common field types
|
|
func StringField(key, value string) zap.Field {
|
|
return zap.String(key, value)
|
|
}
|
|
|
|
func IntField(key string, value int) zap.Field {
|
|
return zap.Int(key, value)
|
|
}
|
|
|
|
func BoolField(key string, value bool) zap.Field {
|
|
return zap.Bool(key, value)
|
|
}
|
|
|
|
func DurationField(key string, value time.Duration) zap.Field {
|
|
return zap.Duration(key, value)
|
|
}
|
|
|
|
func ErrorField(err error) zap.Field {
|
|
return zap.Error(err)
|
|
}
|
|
|
|
func SanitizedURLField(key, rawURL string) zap.Field {
|
|
return zap.String(key, SanitizeURL(rawURL))
|
|
}
|
|
|
|
// WithMCPContext creates a new context with request ID and operation for MCP tool logging
|
|
func WithMCPContext(ctx context.Context, toolName string) (context.Context, string) {
|
|
requestID := GenerateRequestID()
|
|
ctx = WithRequestID(ctx, requestID)
|
|
ctx = WithOperation(ctx, toolName)
|
|
return ctx, requestID
|
|
}
|
|
|
|
// LogMCPToolStart logs the start of an MCP tool execution
|
|
func LogMCPToolStart(ctx context.Context, toolName string, params map[string]interface{}) {
|
|
fields := []zap.Field{
|
|
StringField("tool", toolName),
|
|
}
|
|
|
|
// Add sanitized parameters (be careful not to log sensitive data)
|
|
for key, value := range params {
|
|
if key == "token" || key == "password" || key == "secret" {
|
|
fields = append(fields, StringField(key, "[redacted]"))
|
|
} else {
|
|
fields = append(fields, zap.Any(key, value))
|
|
}
|
|
}
|
|
|
|
InfoCtx(ctx, "Starting MCP tool execution", fields...)
|
|
}
|
|
|
|
// LogMCPToolComplete logs successful completion of an MCP tool execution
|
|
func LogMCPToolComplete(ctx context.Context, toolName string, duration time.Duration, resultSummary string) {
|
|
InfoCtx(ctx, "MCP tool execution completed successfully",
|
|
StringField("tool", toolName),
|
|
DurationField("total_duration", duration),
|
|
StringField("result_summary", resultSummary),
|
|
)
|
|
}
|
|
|
|
// LogMCPToolError logs failed MCP tool execution
|
|
func LogMCPToolError(ctx context.Context, toolName string, duration time.Duration, err error) {
|
|
ErrorCtx(ctx, "MCP tool execution failed",
|
|
StringField("tool", toolName),
|
|
DurationField("total_duration", duration),
|
|
ErrorField(err),
|
|
)
|
|
}
|