debug: make debug mode actually useful for troubleshooting #14
Reference in New Issue
Block a user
Delete Branch "issue-13-structured-debug-logging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #13.
Why
GITEA_DEBUG=trueis the only observability knob the server has, and today it does surprisingly little:Debug(pkg/log/log.go).Text Result: <json>blob frompkg/to/to.go.gitea.SetDebugMode(), which writes to its own stream that does not merge with our log file (pkg/gitea/gitea.go).When a tool call goes wrong, none of that tells you:
--token/GITEA_ACCESS_TOKEN?This PR closes those gaps without changing default behaviour for users who don't enable debug.
What changed
1. Structured tool-invocation logging (
pkg/middleware/logging.go)A new
ToolLoggingmiddleware is registered viaserver.WithToolHandlerMiddleware(...)innewMCPServer. For every tool call it:request_idand stores it in the context together with the tool name.tool,request_id,token_source, andargs(redacted — see §4).duration_msandstatus. Errors are logged aterrorlevel (always on) so even non-debug operators can see which tool failed; previouslyto.ErrorResultdidlog.Errorf("%s", err.Error())with no tool context attached.This replaces the
Text Result:blob inpkg/to.TextResultwith a proper structured entry that includes the tool name, request_id, byte count, and a truncated preview. A newTextResultCtx(ctx, v)variant is provided for new call sites; the legacyTextResultis preserved unchanged for backwards compatibility with the many existing handlers.2. Upstream HTTP logging via a RoundTripper (
pkg/gitea/transport.go)A
loggingRoundTripperwraps the shared*http.Transport. Whenflag.Debugis on, it emits one structured entry per upstream Gitea API call:method,url(with sensitive query params stripped),status,duration_ms,bytes_inrequest_idandtoolpulled fromreq.Context()when available, so HTTP lines can be joined with the middleware's entry/exit linesBecause the RoundTripper sits on the shared transport, both code paths use it:
NewClient(...)(the gitea SDK path) and the hand-rolledDoJSON/DoByteshelpers inrest.go. The latter propagate the tool ctx end-to-end viahttp.NewRequestWithContext, so HTTP log lines from those carry a correlation id; SDK calls don't propagate the ctx (the SDK uses its own background context on the cached client), so they log without a request_id — still useful because method+url+status+duration is the actionable information.gitea.SetDebugMode()is no longer enabled — the RoundTripper supersedes it and integrates with the existing zap log file.3. Effective-config dump at startup (
operation.logEffectiveConfig)When
debug+configscope are on, the server logs version, host, mode, port, read-only, insecure, debug, debug_scopes, token_source, and allowed_tools count exactly once. No token values are ever logged — only the source.This answers "did my env var actually get picked up?" without having to instrument the process.
4. Token source tracking
pkg/flagnow records where the process-wide token came from at startup:flag(CLI),env(GITEA_ACCESS_TOKEN),env-file(GITEA_ACCESS_TOKEN_FILE), or empty. In HTTP mode, when anAuthorizationheader supplies a token,getContextWithTokenstashes"header"as the context-scoped token source. The middleware and the startup dump both report this.5. Baseline redaction (
pkg/debug.RedactArgs/StripQueryTokens)Issue #13 notes that redaction overlaps with #5. This PR adds a minimal redactor — case-insensitive substring match on
token,password,secret,api_key,authorization,auth,credential— so turning on debug isn't an immediate footgun. The package doc explicitly flags this as a placeholder that #5's shared redactor should replace.Result-preview truncation (
debug.TruncatePreview, 1024 runes) is multibyte-safe (operates on runes, not bytes) to avoid splitting a UTF-8 sequence in the log file.6. Stretch goal:
GITEA_DEBUG_SCOPESImplemented as listed in the issue:
GITEA_DEBUG_SCOPES=tools,http,configflag.DebugScopeEnabled(scope)is the single check used by every call siteWhy this shape
A few design choices worth flagging:
WithToolHandlerMiddleware, which lets us instrument all 45 tools centrally. Wrapping each handler would have meant touching everyoperation/*/*.gofile and would have made backports messier.request_idinctx.Value(...). Forpkg/gitea/rest.gothis propagates throughhttp.NewRequestWithContext, so the RoundTripper sees it onreq.Context(). SDK calls lose the linkage because the cached SDK client owns its own context, but that's an acceptable trade for keeping the SDK client cache (which avoids a/api/v1/versionpreflight per call).TextResultleft intact. Every operation module callsto.TextResult(...). Changing its signature would have caused a churn-y diff and conflict with in-flight work.TextResultCtxis provided so new code can opt in to context-aware logging.tool call: erroris now a structurederror-level entry with tool name and request_id. The pre-existing barelog.Errorfinto.ErrorResultlost this context — the middleware restores it.Test coverage
pkg/debug/debug_test.go— argument redaction (nested maps, slices, non-sensitive keys preserved, input not mutated), query-string stripping, multi-byte-safe preview truncation, request-id uniqueness / format, context lookup.pkg/flag/flag_test.go—DebugScopeEnabledsemantics (off when debug off, "everything" when empty, allow-list when populated).pkg/middleware/logging_test.go— context wiring (tool name + request_id reach the handler), error propagation from wrapped handler, token-source resolution order (ctx > flag > "none").go test ./...is green;go vet ./...is clean.Out of scope
pkg/debugand in code comments).to.TextResultcall site toTextResultCtx. Doable as a follow-up; the new variant is in place for it.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.