Compare commits

..

1 Commits

Author SHA1 Message Date
agent-company 5d3ce5baf8 feat: add request-id and duration_ms to structured logging middleware
Enhance the logging middleware with a randomly generated request-id
(X-Request-ID header) for request tracing, duration in milliseconds
for easier metric aggregation, and user-agent for client identification.

Closes leeworks-agents/gitea-mobile#200

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 15:10:08 +00:00
3 changed files with 22 additions and 43 deletions
-34
View File
@@ -78,31 +78,6 @@ func getToken(r *http.Request) string {
return middleware.TokenFromContext(r.Context())
}
// isTokenError returns true if the error indicates an expired or revoked API token.
func isTokenError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "API error 401") || strings.Contains(msg, "API error 403")
}
// redirectOnTokenError checks if the error is a token auth error and redirects
// to /settings with an error banner. Returns true if a redirect was performed.
func redirectOnTokenError(w http.ResponseWriter, r *http.Request, err error) bool {
if !isTokenError(err) {
return false
}
slog.Warn("Gitea API token expired or revoked, redirecting to settings", "error", err)
if isHTMX(r) {
w.Header().Set("HX-Redirect", "/settings?error=token_expired")
w.WriteHeader(http.StatusOK)
} else {
http.Redirect(w, r, "/settings?error=token_expired", http.StatusSeeOther)
}
return true
}
// getUserOrgs returns the list of org names the user belongs to.
func (h *Handler) getUserOrgs(r *http.Request) []string {
token := getToken(r)
@@ -288,9 +263,6 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
queue, err := h.Client.GetTriageQueue(r.Context(), token, queryOrgs)
if err != nil {
if redirectOnTokenError(w, r, err) {
return
}
slog.Error("failed to get triage queue", "error", err)
data.Error = "Error loading triage queue."
} else {
@@ -374,9 +346,6 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
result, err := h.Client.ListAllIssues(r.Context(), token, queryOrgs, selectedState, page, selectedLabel, selectedRepo)
if err != nil {
if redirectOnTokenError(w, r, err) {
return
}
slog.Error("failed to list issues", "error", err)
data.Error = "Error loading issues."
} else {
@@ -482,9 +451,6 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) {
result, err := h.Client.ListAllPullRequests(r.Context(), token, queryOrgs, selectedState, page, selectedLabel, selectedRepo)
if err != nil {
if redirectOnTokenError(w, r, err) {
return
}
slog.Error("failed to list pull requests", "error", err)
data.Error = "Error loading pull requests."
} else {
-7
View File
@@ -45,13 +45,6 @@ func (h *SettingsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
}
data := settingsData{HasToken: hasToken}
// Show error banner when redirected due to expired/revoked token.
if r.URL.Query().Get("error") == "token_expired" {
data.Message = "Your Gitea API token is expired or has been revoked. Please enter a new token."
data.MessageType = "error"
}
h.renderSettings(w, data)
}
+22 -2
View File
@@ -1,6 +1,8 @@
package middleware
import (
"crypto/rand"
"encoding/hex"
"log/slog"
"net/http"
"time"
@@ -17,21 +19,39 @@ func (rw *responseWriter) WriteHeader(code int) {
rw.ResponseWriter.WriteHeader(code)
}
// Logging returns middleware that logs each HTTP request with structured logging.
// generateRequestID creates a short random hex string for request tracing.
func generateRequestID() string {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return "unknown"
}
return hex.EncodeToString(b)
}
// Logging returns middleware that logs each HTTP request with structured fields:
// method, path, status, duration (ms), request-id, and remote address.
func Logging() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
requestID := generateRequestID()
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
// Set request ID header for downstream correlation.
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(rw, r)
duration := time.Since(start)
slog.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"status", rw.statusCode,
"duration", time.Since(start).String(),
"duration_ms", duration.Milliseconds(),
"duration", duration.String(),
"request_id", requestID,
"remote", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}