debug: structured tool/HTTP logging, request IDs, config dump
check-and-test / check-and-test (pull_request) Has been cancelled

Closes #13.

Today GITEA_DEBUG=true only flips the zap level to debug, dumps a
contextless `Text Result:` blob per tool call, and enables the gitea
SDK's own debug stream (which doesn't merge with our log file). That is
not enough to diagnose a misbehaving tool call in production.

This change introduces:

- A `pkg/middleware.ToolLogging` ToolHandlerMiddleware that logs each
  tool invocation on entry and exit with structured fields: tool name,
  redacted args, request_id, token source, duration_ms, and status. The
  request_id is stashed in the context so downstream layers can
  correlate.
- A `loggingRoundTripper` in `pkg/gitea` that wraps the shared HTTP
  transport and emits one structured line per upstream Gitea API call
  (method, token-stripped URL, status, duration_ms, bytes). This
  replaces — and is far more useful than — `gitea.SetDebugMode()`.
- An effective-config dump at startup (`logEffectiveConfig`) when
  debug+config scope is on, so "did my env var get picked up?" becomes
  answerable from the log file.
- Token source tracking. CLI flag / GITEA_ACCESS_TOKEN /
  GITEA_ACCESS_TOKEN_FILE / per-request Authorization header are now
  distinguished in logs as flag/env/env-file/header.
- A baseline redactor in `pkg/debug` that masks args / query params
  whose keys look like secrets (token, password, secret, api_key,
  authorization, …). This is the placeholder for issue #5's shared
  redactor; once that lands, callers here should defer to it.
- `GITEA_DEBUG_SCOPES` env var to scope debug output to any subset of
  `tools,http,config`. Default (unset) is "everything".

Test coverage was added for the redactor, scope flag, request-id
helpers, preview truncation (multibyte-safe), and the middleware's
context wiring + error propagation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 19:04:28 +00:00
parent 4d7a33e57e
commit 316c2c4275
13 changed files with 736 additions and 11 deletions
+49 -1
View File
@@ -27,9 +27,11 @@ import (
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/middleware"
"gitea.com/gitea/gitea-mcp/pkg/tool"
"github.com/mark3labs/mcp-go/server"
"go.uber.org/zap"
)
var (
@@ -80,12 +82,18 @@ func getContextWithToken(ctx context.Context, r *http.Request) context.Context {
return ctx
}
return context.WithValue(ctx, mcpContext.TokenContextKey, token)
ctx = context.WithValue(ctx, mcpContext.TokenContextKey, token)
// Mark per-request header tokens so debug logs can show whether a tool
// call was authenticated via the header (per-request) or fell through
// to the process-wide flag/env token.
ctx = context.WithValue(ctx, mcpContext.TokenSourceContextKey, "header")
return ctx
}
func Run() error {
mcpServer = newMCPServer(flag.Version)
RegisterTool(mcpServer)
logEffectiveConfig()
switch flag.Mode {
case "stdio":
if err := server.ServeStdio(
@@ -135,5 +143,45 @@ func newMCPServer(version string) *server.MCPServer {
server.WithToolCapabilities(true),
server.WithLogging(),
server.WithRecovery(),
// ToolLogging emits structured entry/exit logs and attaches a
// request_id + tool name to the context so downstream layers
// (the gitea RoundTripper, pkg/to result logging) can correlate.
server.WithToolHandlerMiddleware(middleware.ToolLogging),
)
}
// logEffectiveConfig records the runtime configuration once at startup
// when debug mode is on. This makes "did my env var actually take effect?"
// answerable from the log file without having to instrument the process.
//
// Token *values* are never logged — only the source (flag / env / env-file)
// so the operator can tell which auth path was taken.
func logEffectiveConfig() {
if !flag.DebugScopeEnabled("config") {
return
}
tokenSource := flag.TokenSource
if tokenSource == "" {
tokenSource = "none"
}
scopes := "all"
if len(flag.DebugScopes) > 0 {
keys := make([]string, 0, len(flag.DebugScopes))
for k := range flag.DebugScopes {
keys = append(keys, k)
}
scopes = strings.Join(keys, ",")
}
log.Info("gitea-mcp effective config",
zap.String("version", flag.Version),
zap.String("host", flag.Host),
zap.String("mode", flag.Mode),
zap.Int("port", flag.Port),
zap.Bool("read_only", flag.ReadOnly),
zap.Bool("insecure", flag.Insecure),
zap.Bool("debug", flag.Debug),
zap.String("debug_scopes", scopes),
zap.String("token_source", tokenSource),
zap.Int("allowed_tools", len(flag.AllowedTools)),
)
}