Skip to content

Commit 36d23ae

Browse files
systemimeclaude
andcommitted
feat: improve proxy response logging
- Add detailed response logging with method, path, target URL, status, duration, and remote IP - Log response body for 4xx/5xx error responses (truncated to 500 chars) - Move logging from server middleware to proxy module for consistency - Only log local management endpoints in server middleware Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d91315 commit 36d23ae

3 files changed

Lines changed: 51 additions & 14 deletions

File tree

internal/proxy/proxy.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,9 @@ func (p *Proxy) Forward(w http.ResponseWriter, r *http.Request) {
149149

150150
// 处理响应
151151
if resp.StatusCode == http.StatusOK && isEventStream(resp.Header.Get("Content-Type")) {
152-
p.handleStreamResponseWithStats(w, resp, startTime, r.Method, r.URL.Path, model, clientIP, inputTokens, string(body))
152+
p.handleStreamResponseWithStats(w, resp, startTime, r.Method, r.URL.Path, targetURL, model, clientIP, inputTokens, string(body))
153153
} else {
154-
p.handleNormalResponseWithStats(w, resp, startTime, r.Method, r.URL.Path, model, clientIP, inputTokens, string(body))
154+
p.handleNormalResponseWithStats(w, resp, startTime, r.Method, r.URL.Path, targetURL, model, clientIP, inputTokens, string(body))
155155
}
156156
}
157157

@@ -215,7 +215,7 @@ func (p *Proxy) buildHeaders(provider *config.ProviderConfig, apiKey string, req
215215
}
216216

217217
// handleStreamResponseWithStats 处理流式响应并统计
218-
func (p *Proxy) handleStreamResponseWithStats(w http.ResponseWriter, resp *http.Response, startTime time.Time, method, path, model, clientIP string, inputTokens int, requestBody string) {
218+
func (p *Proxy) handleStreamResponseWithStats(w http.ResponseWriter, resp *http.Response, startTime time.Time, method, path, targetURL, model, clientIP string, inputTokens int, requestBody string) {
219219
copyHeaders(w.Header(), resp.Header)
220220

221221
// 设置 SSE 头
@@ -289,7 +289,8 @@ func (p *Proxy) handleStreamResponseWithStats(w http.ResponseWriter, resp *http.
289289
duration := time.Since(startTime).Milliseconds()
290290
totalTokens := inputTokens + outputTokens
291291

292-
p.logForwardResponse(model, outputTokens)
292+
// 打印响应日志
293+
p.logResponse(method, path, targetURL, resp.StatusCode, duration, clientIP, responseBuf.String())
293294

294295
// 保存记录
295296
record := &storage.RequestRecord{
@@ -314,7 +315,7 @@ func (p *Proxy) handleStreamResponseWithStats(w http.ResponseWriter, resp *http.
314315
}
315316

316317
// handleNormalResponseWithStats 处理普通响应并统计
317-
func (p *Proxy) handleNormalResponseWithStats(w http.ResponseWriter, resp *http.Response, startTime time.Time, method, path, model, clientIP string, inputTokens int, requestBody string) {
318+
func (p *Proxy) handleNormalResponseWithStats(w http.ResponseWriter, resp *http.Response, startTime time.Time, method, path, targetURL, model, clientIP string, inputTokens int, requestBody string) {
318319
// 读取响应体
319320
respBody, err := io.ReadAll(resp.Body)
320321
if err != nil {
@@ -350,7 +351,8 @@ func (p *Proxy) handleNormalResponseWithStats(w http.ResponseWriter, resp *http.
350351

351352
totalTokens := inputTokens + outputTokens
352353

353-
p.logForwardResponse(model, outputTokens)
354+
// 打印响应日志
355+
p.logResponse(method, path, targetURL, resp.StatusCode, duration, clientIP, string(respBody))
354356

355357
// 保存记录
356358
record := &storage.RequestRecord{
@@ -510,6 +512,33 @@ func (p *Proxy) logForwardResponse(model string, outputTokens int) {
510512
fmt.Fprintf(p.logOutput(), "时间:%s 转发响应:模型:%s token数:%d\n", humanLogTime(), displayModel(model), outputTokens)
511513
}
512514

515+
// logResponse 打印响应日志
516+
func (p *Proxy) logResponse(method, path, targetURL string, statusCode int, duration int64, clientIP, responseBody string) {
517+
// 判断是否是错误状态码 (4xx 或 5xx)
518+
isError := statusCode >= 400 && statusCode < 600
519+
520+
fields := []zap.Field{
521+
zap.String("method", method),
522+
zap.String("path", path),
523+
zap.String("target", targetURL),
524+
zap.Int("status", statusCode),
525+
zap.Int64("duration_ms", duration),
526+
zap.String("remote", clientIP),
527+
}
528+
529+
if isError {
530+
// 限制响应体长度,避免日志过大
531+
truncatedBody := responseBody
532+
if len(truncatedBody) > 500 {
533+
truncatedBody = truncatedBody[:500] + "...(truncated)"
534+
}
535+
fields = append(fields, zap.String("response", truncatedBody))
536+
p.logger.Warn("代理响应", fields...)
537+
} else {
538+
p.logger.Info("代理响应", fields...)
539+
}
540+
}
541+
513542
func (p *Proxy) logOutput() io.Writer {
514543
if p.output != nil {
515544
return p.output

internal/proxy/proxy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ func TestHandleStreamResponsePreservesEventBoundaries(t *testing.T) {
185185
Body: io.NopCloser(strings.NewReader("data: {\"usage\":{\"completion_tokens\":3}}\n\ndata: [DONE]\n\n")),
186186
}
187187

188-
p.handleStreamResponseWithStats(recorder, resp, time.Now(), http.MethodPost, "/chat/completions", "glm-4-flash", "127.0.0.1", 2, "{}")
188+
p.handleStreamResponseWithStats(recorder, resp, time.Now(), http.MethodPost, "/chat/completions", "https://api.example.com/chat/completions", "glm-4-flash", "127.0.0.1", 2, "{}")
189189

190190
body := recorder.Body.String()
191191
if !strings.Contains(body, "\n\n") {

internal/server/server.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,16 +156,24 @@ func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
156156

157157
duration := time.Since(start)
158158

159-
s.logger.Info("HTTP 请求",
160-
zap.String("method", r.Method),
161-
zap.String("path", r.URL.Path),
162-
zap.Int("status", wrapped.statusCode),
163-
zap.Duration("duration", duration),
164-
zap.String("remote", r.RemoteAddr),
165-
)
159+
// 只记录本地管理端点的日志,代理请求由 proxy 模块记录详细日志
160+
if isLocalEndpoint(r.URL.Path) {
161+
s.logger.Info("HTTP 请求",
162+
zap.String("method", r.Method),
163+
zap.String("path", r.URL.Path),
164+
zap.Int("status", wrapped.statusCode),
165+
zap.Duration("duration", duration),
166+
zap.String("remote", r.RemoteAddr),
167+
)
168+
}
166169
})
167170
}
168171

172+
// isLocalEndpoint 判断是否是本地管理端点
173+
func isLocalEndpoint(path string) bool {
174+
return path == "/" || path == "/health" || path == "/ready" || path == "/stats"
175+
}
176+
169177
// securityMiddleware 安全头中间件
170178
func (s *Server) securityMiddleware(next http.Handler) http.Handler {
171179
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)