Initial commit: Gitea MCP Server
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
// Package log provides structured logging with context support for request tracing.
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// contextKey is a type for context keys to avoid collisions.
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
// correlationIDKey is the context key for correlation ID.
|
||||
correlationIDKey contextKey = "correlation_id"
|
||||
// operationKey is the context key for operation name.
|
||||
operationKey contextKey = "operation"
|
||||
// startTimeKey is the context key for operation start time.
|
||||
startTimeKey contextKey = "start_time"
|
||||
)
|
||||
|
||||
var (
|
||||
// correlationIDGenerator provides thread-safe ID generation.
|
||||
correlationIDGenerator = &idGenerator{}
|
||||
)
|
||||
|
||||
// idGenerator generates unique correlation IDs.
|
||||
type idGenerator struct {
|
||||
mu sync.Mutex
|
||||
seq uint64
|
||||
}
|
||||
|
||||
// Generate creates a new unique correlation ID.
|
||||
func (g *idGenerator) Generate() string {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.seq++
|
||||
return time.Now().Format("20060102-150405") + "-" + string(rune(g.seq))
|
||||
}
|
||||
|
||||
// WithCorrelationID adds a correlation ID to the context for request tracing.
|
||||
// If no ID is provided, a new one will be generated.
|
||||
func WithCorrelationID(ctx context.Context, id string) context.Context {
|
||||
if id == "" {
|
||||
id = correlationIDGenerator.Generate()
|
||||
}
|
||||
return context.WithValue(ctx, correlationIDKey, id)
|
||||
}
|
||||
|
||||
// WithOperation adds an operation name to the context.
|
||||
func WithOperation(ctx context.Context, operation string) context.Context {
|
||||
return context.WithValue(ctx, operationKey, operation)
|
||||
}
|
||||
|
||||
// WithStartTime adds operation start time to the context.
|
||||
func WithStartTime(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, startTimeKey, time.Now())
|
||||
}
|
||||
|
||||
// GetCorrelationID retrieves the correlation ID from context.
|
||||
func GetCorrelationID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if id, ok := ctx.Value(correlationIDKey).(string); ok {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetOperation retrieves the operation name from context.
|
||||
func GetOperation(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if op, ok := ctx.Value(operationKey).(string); ok {
|
||||
return op
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetStartTime retrieves the operation start time from context.
|
||||
func GetStartTime(ctx context.Context) time.Time {
|
||||
if ctx == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if t, ok := ctx.Value(startTimeKey).(time.Time); ok {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Duration returns the elapsed time since the operation started.
|
||||
// Returns 0 if no start time is set in context.
|
||||
func Duration(ctx context.Context) time.Duration {
|
||||
start := GetStartTime(ctx)
|
||||
if start.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return time.Since(start)
|
||||
}
|
||||
|
||||
// Logger provides request-scoped logging with context fields.
|
||||
type Logger struct {
|
||||
*zap.Logger
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// WithContext creates a new Logger with context fields.
|
||||
func WithContext(ctx context.Context) *Logger {
|
||||
return &Logger{
|
||||
Logger: Default(),
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger creates a new Logger with a specific zap logger and context.
|
||||
func WithLogger(logger *zap.Logger, ctx context.Context) *Logger {
|
||||
return &Logger{
|
||||
Logger: logger,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// contextFields returns zap fields from context values.
|
||||
func (l *Logger) contextFields() []zap.Field {
|
||||
if l.ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fields []zap.Field
|
||||
|
||||
if id := GetCorrelationID(l.ctx); id != "" {
|
||||
fields = append(fields, zap.String("correlation_id", id))
|
||||
}
|
||||
|
||||
if op := GetOperation(l.ctx); op != "" {
|
||||
fields = append(fields, zap.String("operation", op))
|
||||
}
|
||||
|
||||
if start := GetStartTime(l.ctx); !start.IsZero() {
|
||||
fields = append(fields, zap.Duration("duration", time.Since(start)))
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// Debug logs a message at debug level with context fields.
|
||||
func (l *Logger) Debug(msg string, fields ...zap.Field) {
|
||||
allFields := append(l.contextFields(), fields...)
|
||||
l.Logger.Debug(msg, allFields...)
|
||||
}
|
||||
|
||||
// Info logs a message at info level with context fields.
|
||||
func (l *Logger) Info(msg string, fields ...zap.Field) {
|
||||
allFields := append(l.contextFields(), fields...)
|
||||
l.Logger.Info(msg, allFields...)
|
||||
}
|
||||
|
||||
// Warn logs a message at warn level with context fields.
|
||||
func (l *Logger) Warn(msg string, fields ...zap.Field) {
|
||||
allFields := append(l.contextFields(), fields...)
|
||||
l.Logger.Warn(msg, allFields...)
|
||||
}
|
||||
|
||||
// Error logs a message at error level with context fields.
|
||||
func (l *Logger) Error(msg string, fields ...zap.Field) {
|
||||
allFields := append(l.contextFields(), fields...)
|
||||
l.Logger.Error(msg, allFields...)
|
||||
}
|
||||
|
||||
// Fatal logs a message at fatal level with context fields.
|
||||
func (l *Logger) Fatal(msg string, fields ...zap.Field) {
|
||||
allFields := append(l.contextFields(), fields...)
|
||||
l.Logger.Fatal(msg, allFields...)
|
||||
}
|
||||
|
||||
// Panic logs a message at panic level with context fields.
|
||||
func (l *Logger) Panic(msg string, fields ...zap.Field) {
|
||||
allFields := append(l.contextFields(), fields...)
|
||||
l.Logger.Panic(msg, allFields...)
|
||||
}
|
||||
|
||||
// Debugf logs a formatted message at debug level with context fields.
|
||||
func (l *Logger) Debugf(format string, args ...any) {
|
||||
l.Debug(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Infof logs a formatted message at info level with context fields.
|
||||
func (l *Logger) Infof(format string, args ...any) {
|
||||
l.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warnf logs a formatted message at warn level with context fields.
|
||||
func (l *Logger) Warnf(format string, args ...any) {
|
||||
l.Warn(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Errorf logs a formatted message at error level with context fields.
|
||||
func (l *Logger) Errorf(format string, args ...any) {
|
||||
l.Error(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted message at fatal level with context fields.
|
||||
func (l *Logger) Fatalf(format string, args ...any) {
|
||||
l.Fatal(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Panicf logs a formatted message at panic level with context fields.
|
||||
func (l *Logger) Panicf(format string, args ...any) {
|
||||
l.Panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// With creates a child logger with additional fields.
|
||||
func (l *Logger) With(fields ...zap.Field) *Logger {
|
||||
return &Logger{
|
||||
Logger: l.Logger.With(fields...),
|
||||
ctx: l.ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// WithError creates an error log entry with error details.
|
||||
func (l *Logger) WithError(err error) *ErrorEntry {
|
||||
return &ErrorEntry{
|
||||
Logger: l,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorEntry provides structured error logging.
|
||||
type ErrorEntry struct {
|
||||
*Logger
|
||||
err error
|
||||
}
|
||||
|
||||
// Log logs the error with additional context.
|
||||
func (e *ErrorEntry) Log(msg string) {
|
||||
if e.err == nil {
|
||||
e.Error(msg)
|
||||
return
|
||||
}
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.Error(e.err),
|
||||
zap.String("error_type", fmt.Sprintf("%T", e.err)),
|
||||
}
|
||||
|
||||
e.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// LogWithStatus logs the error with HTTP status code.
|
||||
func (e *ErrorEntry) LogWithStatus(msg string, statusCode int) {
|
||||
if e.err == nil {
|
||||
e.Error(msg, zap.Int("status_code", statusCode))
|
||||
return
|
||||
}
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.Error(e.err),
|
||||
zap.String("error_type", fmt.Sprintf("%T", e.err)),
|
||||
zap.Int("status_code", statusCode),
|
||||
}
|
||||
|
||||
e.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// OperationLogger provides a convenient way to log operation execution.
|
||||
type OperationLogger struct {
|
||||
logger *Logger
|
||||
operation string
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// StartOperation begins logging an operation with timing.
|
||||
func StartOperation(ctx context.Context, operation string) *OperationLogger {
|
||||
logger := WithContext(WithOperation(ctx, operation))
|
||||
logger.ctx = WithStartTime(logger.ctx)
|
||||
|
||||
return &OperationLogger{
|
||||
logger: logger,
|
||||
operation: operation,
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the operation logging with a start message.
|
||||
func (o *OperationLogger) Start(msg string) {
|
||||
o.logger.Info(msg, zap.String("phase", "start"))
|
||||
}
|
||||
|
||||
// Success logs a successful operation completion.
|
||||
func (o *OperationLogger) Success(msg string) {
|
||||
duration := time.Since(o.start)
|
||||
o.logger.Info(msg,
|
||||
zap.String("phase", "complete"),
|
||||
zap.String("status", "success"),
|
||||
zap.Duration("duration", duration),
|
||||
)
|
||||
}
|
||||
|
||||
// Failure logs a failed operation.
|
||||
func (o *OperationLogger) Failure(msg string, err error) {
|
||||
duration := time.Since(o.start)
|
||||
fields := []zap.Field{
|
||||
zap.String("phase", "complete"),
|
||||
zap.String("status", "failure"),
|
||||
zap.Duration("duration", duration),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
}
|
||||
|
||||
o.logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Duration returns the elapsed time since operation started.
|
||||
func (o *OperationLogger) Duration() time.Duration {
|
||||
return time.Since(o.start)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLoggerOnce sync.Once
|
||||
defaultLogger *zap.Logger
|
||||
)
|
||||
|
||||
func Default() *zap.Logger {
|
||||
defaultLoggerOnce.Do(func() {
|
||||
if defaultLogger != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ec := zap.NewProductionEncoderConfig()
|
||||
ec.EncodeTime = zapcore.TimeEncoderOfLayout(time.DateTime)
|
||||
ec.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
|
||||
var ws zapcore.WriteSyncer
|
||||
var wss []zapcore.WriteSyncer
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" {
|
||||
home = os.TempDir()
|
||||
}
|
||||
|
||||
logDir := home + "/.gitea-mcp"
|
||||
if err := os.MkdirAll(logDir, 0o700); err != nil {
|
||||
// Fallback to temp directory if creation fails
|
||||
logDir = os.TempDir()
|
||||
}
|
||||
|
||||
wss = append(wss, zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: logDir + "/gitea-mcp.log",
|
||||
MaxSize: 100,
|
||||
MaxBackups: 10,
|
||||
MaxAge: 30,
|
||||
}))
|
||||
|
||||
if flag.Mode == "http" {
|
||||
wss = append(wss, zapcore.AddSync(os.Stdout))
|
||||
}
|
||||
|
||||
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.DPanicLevel),
|
||||
zap.AddCaller(),
|
||||
zap.AddCallerSkip(1),
|
||||
}
|
||||
defaultLogger = zap.New(core, options...)
|
||||
})
|
||||
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
func SetDefault(logger *zap.Logger) {
|
||||
if logger != nil {
|
||||
defaultLogger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Logger with the default zap logger.
|
||||
// This is a compatibility wrapper for the MCP server.
|
||||
func New() *Logger {
|
||||
return WithContext(context.Background())
|
||||
}
|
||||
|
||||
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 Fatalf(format string, args ...any) {
|
||||
Default().Sugar().Fatalf(format, args...)
|
||||
}
|
||||
Reference in New Issue
Block a user