refactor(proxy): migrate to zerolog structured logging

This commit is contained in:
Alexander
2026-04-10 18:19:13 +02:00
parent 76bf651742
commit 4e22c463cf
3 changed files with 91 additions and 17 deletions
+82 -12
View File
@@ -3,13 +3,15 @@ package proxy
import ( import (
"bufio" "bufio"
"io" "io"
"log"
"net/http" "net/http"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/fujin/anthropic-proxy/internal/auth" "github.com/fujin/anthropic-proxy/internal/auth"
"github.com/fujin/anthropic-proxy/internal/logging"
) )
func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func() *Sanitizer) gin.HandlerFunc { func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func() *Sanitizer) gin.HandlerFunc {
@@ -22,7 +24,15 @@ func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func(
return return
} }
log.Printf("incoming: %s %s (%d bytes) model=%s", c.Request.Method, c.Request.URL.Path, len(body), gjson.GetBytes(body, "model").String()) originalBody := make([]byte, len(body))
copy(originalBody, body)
log.Info().
Str("method", c.Request.Method).
Str("path", c.Request.URL.Path).
Int("body_size", len(body)).
Str("model", gjson.GetBytes(body, "model").String()).
Msg("incoming request")
san := getSanitizer() san := getSanitizer()
body = san.SanitizeRequest(body) body = san.SanitizeRequest(body)
@@ -36,24 +46,54 @@ func HandleMessages(pool *auth.Pool, profile *SniffedProfile, getSanitizer func(
isStream := gjson.GetBytes(body, "stream").Bool() isStream := gjson.GetBytes(body, "stream").Bool()
if isStream { if isStream {
handleStream(c, upstream, san, pool, cred, body) handleStream(c, upstream, san, pool, cred, body, originalBody)
} else { } else {
handleNonStream(c, upstream, san, pool, cred, body) handleNonStream(c, upstream, san, pool, cred, body, originalBody)
} }
} }
} }
func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte) { func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
startTime := time.Now()
respBody, headers, statusCode, err := upstream.Execute(c.Request.Context(), cred, body) respBody, headers, statusCode, err := upstream.Execute(c.Request.Context(), cred, body)
if err != nil { if err != nil {
log.Printf("upstream error for %s: %v", cred.Email, err) latencyMs := float64(time.Since(startTime).Milliseconds())
model := gjson.GetBytes(body, "model").String()
log.Error().
Err(err).
Str("credential", cred.Email).
Str("model", model).
Bool("stream", false).
Str("request_body_original", string(originalBody)).
Str("request_body_sanitized", string(body)).
Int("request_body_size", len(body)).
Float64("latency_ms", latencyMs).
Msg("upstream connection error")
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed"}) c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed"})
return return
} }
if statusCode >= 400 { if statusCode >= 400 {
pool.MarkFailure(cred, statusCode) pool.MarkFailure(cred, statusCode)
log.Printf("upstream %d for %s: %s", statusCode, cred.Email, string(respBody)) latencyMs := float64(time.Since(startTime).Milliseconds())
model := gjson.GetBytes(body, "model").String()
errorType := gjson.GetBytes(respBody, "error.type").String()
errorMessage := gjson.GetBytes(respBody, "error.message").String()
log.Error().
Int("status", statusCode).
Str("error_type", errorType).
Str("error_message", errorMessage).
Str("response_body", string(respBody)).
Str("request_id", headers.Get("X-Request-Id")).
Float64("latency_ms", latencyMs).
Str("credential", cred.Email).
Str("model", model).
Bool("stream", false).
Str("request_body_original", string(originalBody)).
Str("request_body_sanitized", string(body)).
Int("request_body_size", len(body)).
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
Msg("upstream error")
} else { } else {
pool.MarkSuccess(cred) pool.MarkSuccess(cred)
respBody = san.DesanitizeResponse(respBody) respBody = san.DesanitizeResponse(respBody)
@@ -68,10 +108,22 @@ func handleNonStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, p
c.Data(statusCode, headers.Get("Content-Type"), respBody) c.Data(statusCode, headers.Get("Content-Type"), respBody)
} }
func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte) { func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool *auth.Pool, cred *auth.Credential, body []byte, originalBody []byte) {
startTime := time.Now()
resp, err := upstream.ExecuteStream(c.Request.Context(), cred, body) resp, err := upstream.ExecuteStream(c.Request.Context(), cred, body)
if err != nil { if err != nil {
log.Printf("upstream stream error for %s: %v", cred.Email, err) latencyMs := float64(time.Since(startTime).Milliseconds())
model := gjson.GetBytes(body, "model").String()
log.Error().
Err(err).
Str("credential", cred.Email).
Str("model", model).
Bool("stream", true).
Str("request_body_original", string(originalBody)).
Str("request_body_sanitized", string(body)).
Int("request_body_size", len(body)).
Float64("latency_ms", latencyMs).
Msg("upstream connection error")
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream stream request failed"}) c.JSON(http.StatusBadGateway, gin.H{"error": "upstream stream request failed"})
return return
} }
@@ -80,7 +132,25 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
pool.MarkFailure(cred, resp.StatusCode) pool.MarkFailure(cred, resp.StatusCode)
respBody, _ := io.ReadAll(resp.Body) respBody, _ := io.ReadAll(resp.Body)
log.Printf("upstream stream %d for %s: %s", resp.StatusCode, cred.Email, string(respBody)) latencyMs := float64(time.Since(startTime).Milliseconds())
model := gjson.GetBytes(body, "model").String()
errorType := gjson.GetBytes(respBody, "error.type").String()
errorMessage := gjson.GetBytes(respBody, "error.message").String()
log.Error().
Int("status", resp.StatusCode).
Str("error_type", errorType).
Str("error_message", errorMessage).
Str("response_body", string(respBody)).
Str("request_id", resp.Header.Get("X-Request-Id")).
Float64("latency_ms", latencyMs).
Str("credential", cred.Email).
Str("model", model).
Bool("stream", true).
Str("request_body_original", string(originalBody)).
Str("request_body_sanitized", string(body)).
Int("request_body_size", len(body)).
Str("request_headers", logging.RedactHeaders(c.Request.Header)).
Msg("upstream error")
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody) c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
return return
} }
@@ -94,7 +164,7 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
flusher, ok := c.Writer.(http.Flusher) flusher, ok := c.Writer.(http.Flusher)
if !ok { if !ok {
log.Printf("response writer does not support flushing") log.Error().Msg("response writer does not support flushing")
c.JSON(http.StatusInternalServerError, gin.H{"error": "streaming not supported"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "streaming not supported"})
return return
} }
@@ -108,6 +178,6 @@ func handleStream(c *gin.Context, upstream *UpstreamClient, san *Sanitizer, pool
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Printf("stream scan error: %v", err) log.Error().Err(err).Msg("stream scan error")
} }
} }
+7 -3
View File
@@ -3,13 +3,14 @@ package proxy
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"net/http" "net/http"
"os/exec" "os/exec"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog/log"
) )
// SniffedProfile holds everything captured from a real Claude Code request. // SniffedProfile holds everything captured from a real Claude Code request.
@@ -116,8 +117,11 @@ func SniffClaudeCode(claudeBinary string) (*SniffedProfile, error) {
return nil, fmt.Errorf("no API request captured") return nil, fmt.Errorf("no API request captured")
} }
log.Printf("sniffed claude-code: version=%s headers=%d body=%d bytes", log.Info().
profile.Version, len(profile.Headers), len(profile.Body)) Str("version", profile.Version).
Int("headers", len(profile.Headers)).
Int("body_size", len(profile.Body)).
Msg("sniffed claude-code profile")
return profile, nil return profile, nil
} }
+2 -2
View File
@@ -1,12 +1,12 @@
package proxy package proxy
import ( import (
"log"
"net" "net"
"net/http" "net/http"
"sync" "sync"
tls "github.com/refraction-networking/utls" tls "github.com/refraction-networking/utls"
"github.com/rs/zerolog/log"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@@ -90,7 +90,7 @@ func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
port = "443" port = "443"
} }
addr := net.JoinHostPort(hostname, port) addr := net.JoinHostPort(hostname, port)
log.Printf("utls: RoundTrip to %s (Chrome TLS fingerprint, HTTP/2)", addr) log.Debug().Str("addr", addr).Msg("uTLS round trip")
h2Conn, err := t.getOrCreateConnection(hostname, addr) h2Conn, err := t.getOrCreateConnection(hostname, addr)
if err != nil { if err != nil {