Skip to content

Commit 54f5616

Browse files
LimLLLlimliu
andauthored
feat: 添加请求和响应体日志记录功能 (#180)
* feat: 添加请求和响应体日志记录功能 - 在 RequestLog 模型中添加 request_body 和 response_body 字段用于存储完整的请求和响应内容 - 修改响应处理器以捕获和返回响应体内容,支持流式和非流式响应 - 更新代理服务器逻辑,在请求日志中记录请求体和响应体 - 在前端日志表格中添加详情查看功能,支持查看完整的请求和响应内容 - 提供 JSON 格式化显示和语法高亮,提升日志查看体验 * feat: 添加是否记录响应和请求体按钮 * fix: 修改请求和响应体记录选项的默认值为false * feat: 添加分组级请求体日志记录控制功能 - 分组 - 高级配置 - 分组配置添加"禁用请求和响应体日志记录"选项 (disable_request_body_logging),覆盖系统设置 * refactor: 重构请求体响应体记录分组级别逻辑 - 不再单独处理分组设置,系统和分组使用同一个配置key - 具体逻辑: - 系统启用 + 分组不做配置 = 记录 - 系统启用 + 分组启用 = 记录 - 系统启用 + 分组禁用 = 未记录请求内容(此分组已禁用请求体记录功能) - 系统禁用 + 分组开启 = 未记录请求内容(系统设置中已关闭请求体记录功能) - 系统禁用 = 未记录请求内容(系统设置中已关闭请求体记录功能) --------- Co-authored-by: limliu <[email protected]>
1 parent 0bcc068 commit 54f5616

File tree

10 files changed

+284
-35
lines changed

10 files changed

+284
-35
lines changed

internal/config/system_settings.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ func (sm *SystemSettingsManager) ValidateGroupConfigOverrides(configMap map[stri
334334
continue
335335
}
336336

337+
337338
field, ok := jsonToField[key]
338339
if !ok {
339340
return fmt.Errorf("invalid setting key: %s", key)
@@ -377,6 +378,11 @@ func (sm *SystemSettingsManager) ValidateGroupConfigOverrides(configMap map[stri
377378
}
378379
}
379380
}
381+
case reflect.Bool:
382+
_, ok := value.(bool)
383+
if !ok {
384+
return fmt.Errorf("invalid type for %s: expected boolean, got %T", key, value)
385+
}
380386
default:
381387
// Do not validate other types for group overrides
382388
}

internal/models/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type GroupConfig struct {
3737
KeyValidationIntervalMinutes *int `json:"key_validation_interval_minutes,omitempty"`
3838
KeyValidationConcurrency *int `json:"key_validation_concurrency,omitempty"`
3939
KeyValidationTimeoutSeconds *int `json:"key_validation_timeout_seconds,omitempty"`
40+
EnableRequestBodyLogging *bool `json:"enable_request_body_logging,omitempty"`
4041
}
4142

4243
// HeaderRule defines a single rule for header manipulation.
@@ -104,6 +105,9 @@ type RequestLog struct {
104105
Retries int `gorm:"not null" json:"retries"`
105106
UpstreamAddr string `gorm:"type:varchar(500)" json:"upstream_addr"`
106107
IsStream bool `gorm:"not null" json:"is_stream"`
108+
RequestBody string `gorm:"type:longtext" json:"request_body"`
109+
ResponseBody string `gorm:"type:longtext" json:"response_body"`
110+
BodyLogStatus string `gorm:"type:varchar(50)" json:"body_log_status"` // "enabled", "system_disabled", "group_disabled"
107111
}
108112

109113
// StatCard 用于仪表盘的单个统计卡片数据
Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package proxy
22

33
import (
4+
"bytes"
45
"io"
56
"net/http"
67

78
"github.com/gin-gonic/gin"
89
"github.com/sirupsen/logrus"
910
)
1011

11-
func (ps *ProxyServer) handleStreamingResponse(c *gin.Context, resp *http.Response) {
12+
func (ps *ProxyServer) handleStreamingResponse(c *gin.Context, resp *http.Response) string {
1213
c.Header("Content-Type", "text/event-stream")
1314
c.Header("Cache-Control", "no-cache")
1415
c.Header("Connection", "keep-alive")
@@ -17,32 +18,46 @@ func (ps *ProxyServer) handleStreamingResponse(c *gin.Context, resp *http.Respon
1718
flusher, ok := c.Writer.(http.Flusher)
1819
if !ok {
1920
logrus.Error("Streaming unsupported by the writer, falling back to normal response")
20-
ps.handleNormalResponse(c, resp)
21-
return
21+
return ps.handleNormalResponse(c, resp)
2222
}
2323

24+
var responseBuffer bytes.Buffer
2425
buf := make([]byte, 4*1024)
2526
for {
2627
n, err := resp.Body.Read(buf)
2728
if n > 0 {
29+
// Write to client
2830
if _, writeErr := c.Writer.Write(buf[:n]); writeErr != nil {
2931
logUpstreamError("writing stream to client", writeErr)
30-
return
32+
return responseBuffer.String()
3133
}
34+
// Also capture for logging
35+
responseBuffer.Write(buf[:n])
3236
flusher.Flush()
3337
}
3438
if err == io.EOF {
3539
break
3640
}
3741
if err != nil {
3842
logUpstreamError("reading from upstream", err)
39-
return
43+
return responseBuffer.String()
4044
}
4145
}
46+
return responseBuffer.String()
4247
}
4348

44-
func (ps *ProxyServer) handleNormalResponse(c *gin.Context, resp *http.Response) {
45-
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
49+
func (ps *ProxyServer) handleNormalResponse(c *gin.Context, resp *http.Response) string {
50+
// Read the response body
51+
responseBody, err := io.ReadAll(resp.Body)
52+
if err != nil {
53+
logUpstreamError("reading response body", err)
54+
return ""
55+
}
56+
57+
// Write to client
58+
if _, err := c.Writer.Write(responseBody); err != nil {
4659
logUpstreamError("copying response body", err)
4760
}
61+
62+
return string(responseBody)
4863
}

internal/proxy/server.go

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ func (ps *ProxyServer) executeRequestWithRetry(
114114
}
115115
logrus.Debugf("Max retries exceeded for group %s after %d attempts. Parsed Error: %s", group.Name, retryCount, logMessage)
116116

117-
ps.logRequest(c, group, &models.APIKey{KeyValue: lastError.KeyValue}, startTime, lastError.StatusCode, retryCount, errors.New(logMessage), isStream, lastError.UpstreamAddr, channelHandler, bodyBytes)
117+
ps.logRequest(c, group, &models.APIKey{KeyValue: lastError.KeyValue}, startTime, lastError.StatusCode, retryCount, errors.New(logMessage), isStream, lastError.UpstreamAddr, channelHandler, bodyBytes, "")
118118
} else {
119119
response.Error(c, app_errors.ErrMaxRetriesExceeded)
120120
logrus.Debugf("Max retries exceeded for group %s after %d attempts.", group.Name, retryCount)
121-
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, app_errors.ErrMaxRetriesExceeded, isStream, "", channelHandler, bodyBytes)
121+
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, app_errors.ErrMaxRetriesExceeded, isStream, "", channelHandler, bodyBytes, "")
122122
}
123123
return
124124
}
@@ -127,7 +127,7 @@ func (ps *ProxyServer) executeRequestWithRetry(
127127
if err != nil {
128128
logrus.Errorf("Failed to select a key for group %s on attempt %d: %v", group.Name, retryCount+1, err)
129129
response.Error(c, app_errors.NewAPIError(app_errors.ErrNoKeysAvailable, err.Error()))
130-
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, err, isStream, "", channelHandler, bodyBytes)
130+
ps.logRequest(c, group, nil, startTime, http.StatusServiceUnavailable, retryCount, err, isStream, "", channelHandler, bodyBytes, "")
131131
return
132132
}
133133

@@ -191,7 +191,7 @@ func (ps *ProxyServer) executeRequestWithRetry(
191191
if err != nil || (resp != nil && resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound) {
192192
if err != nil && app_errors.IsIgnorableError(err) {
193193
logrus.Debugf("Client-side ignorable error for key %s, aborting retries: %v", utils.MaskAPIKey(apiKey.KeyValue), err)
194-
ps.logRequest(c, group, apiKey, startTime, 499, retryCount+1, err, isStream, upstreamURL, channelHandler, bodyBytes)
194+
ps.logRequest(c, group, apiKey, startTime, 499, retryCount+1, err, isStream, upstreamURL, channelHandler, bodyBytes, "")
195195
return
196196
}
197197

@@ -236,7 +236,6 @@ func (ps *ProxyServer) executeRequestWithRetry(
236236

237237
// ps.keyProvider.UpdateStatus(apiKey, group, true) // 请求成功不再重置成功次数,减少IO消耗
238238
logrus.Debugf("Request for group %s succeeded on attempt %d with key %s", group.Name, retryCount+1, utils.MaskAPIKey(apiKey.KeyValue))
239-
ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, retryCount+1, nil, isStream, upstreamURL, channelHandler, bodyBytes)
240239

241240
for key, values := range resp.Header {
242241
for _, value := range values {
@@ -245,11 +244,14 @@ func (ps *ProxyServer) executeRequestWithRetry(
245244
}
246245
c.Status(resp.StatusCode)
247246

247+
var responseBody string
248248
if isStream {
249-
ps.handleStreamingResponse(c, resp)
249+
responseBody = ps.handleStreamingResponse(c, resp)
250250
} else {
251-
ps.handleNormalResponse(c, resp)
251+
responseBody = ps.handleNormalResponse(c, resp)
252252
}
253+
254+
ps.logRequest(c, group, apiKey, startTime, resp.StatusCode, retryCount+1, nil, isStream, upstreamURL, channelHandler, bodyBytes, responseBody)
253255
}
254256

255257
// logRequest is a helper function to create and record a request log.
@@ -265,25 +267,56 @@ func (ps *ProxyServer) logRequest(
265267
upstreamAddr string,
266268
channelHandler channel.ChannelProxy,
267269
bodyBytes []byte,
270+
responseBody string,
268271
) {
269272
if ps.requestLogService == nil {
270273
return
271274
}
272275

273276
duration := time.Since(startTime).Milliseconds()
274277

278+
// 日志记录逻辑:系统开启 AND 分组开启 = 记录
279+
var requestBodyToLog, responseBodyToLog string
280+
var bodyLogStatus string
281+
282+
systemEnabled := ps.settingsManager.GetSettings().EnableRequestBodyLogging
283+
groupEnabled := true // 默认分组开启
284+
285+
// 检查分组配置中的设置
286+
if group.Config != nil {
287+
if enableValue, exists := group.Config["enable_request_body_logging"]; exists {
288+
if enable, ok := enableValue.(bool); ok {
289+
groupEnabled = enable
290+
}
291+
}
292+
}
293+
294+
// 只有系统和分组都开启才记录
295+
if !systemEnabled {
296+
bodyLogStatus = "system_disabled"
297+
} else if !groupEnabled {
298+
bodyLogStatus = "group_disabled"
299+
} else {
300+
bodyLogStatus = "enabled"
301+
requestBodyToLog = string(bodyBytes)
302+
responseBodyToLog = responseBody
303+
}
304+
275305
logEntry := &models.RequestLog{
276-
GroupID: group.ID,
277-
GroupName: group.Name,
278-
IsSuccess: finalError == nil && statusCode < 400,
279-
SourceIP: c.ClientIP(),
280-
StatusCode: statusCode,
281-
RequestPath: utils.TruncateString(c.Request.URL.String(), 500),
282-
Duration: duration,
283-
UserAgent: c.Request.UserAgent(),
284-
Retries: retries,
285-
IsStream: isStream,
286-
UpstreamAddr: utils.TruncateString(upstreamAddr, 500),
306+
GroupID: group.ID,
307+
GroupName: group.Name,
308+
IsSuccess: finalError == nil && statusCode < 400,
309+
SourceIP: c.ClientIP(),
310+
StatusCode: statusCode,
311+
RequestPath: utils.TruncateString(c.Request.URL.String(), 500),
312+
Duration: duration,
313+
UserAgent: c.Request.UserAgent(),
314+
Retries: retries,
315+
IsStream: isStream,
316+
UpstreamAddr: utils.TruncateString(upstreamAddr, 500),
317+
RequestBody: requestBodyToLog,
318+
ResponseBody: responseBodyToLog,
319+
BodyLogStatus: bodyLogStatus,
287320
}
288321

289322
if channelHandler != nil && bodyBytes != nil {

internal/types/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type SystemSettings struct {
2424
ProxyKeys string `json:"proxy_keys" name:"全局代理密钥" category:"基础参数" desc:"全局代理密钥,用于访问所有分组的代理端点。多个密钥请用逗号分隔。" validate:"required"`
2525

2626
// 请求设置
27+
EnableRequestBodyLogging bool `json:"enable_request_body_logging" default:"false" name:"记录请求和响应体" category:"请求设置" desc:"是否在请求日志中记录完整的请求体和响应体内容。关闭此选项可以减少存储空间占用。"`
2728
RequestTimeout int `json:"request_timeout" default:"600" name:"请求超时(秒)" category:"请求设置" desc:"转发请求的完整生命周期超时(秒)等。" validate:"required,min=1"`
2829
ConnectTimeout int `json:"connect_timeout" default:"15" name:"连接超时(秒)" category:"请求设置" desc:"与上游服务建立新连接的超时时间(秒)。" validate:"required,min=1"`
2930
IdleConnTimeout int `json:"idle_conn_timeout" default:"120" name:"空闲连接超时(秒)" category:"请求设置" desc:"HTTP 客户端中空闲连接的超时时间(秒)。" validate:"required,min=1"`

web/src/api/settings.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import http from "@/utils/http";
33
export interface Setting {
44
key: string;
55
name: string;
6-
value: string | number;
7-
type: "int" | "string";
6+
value: string | number | boolean;
7+
type: "int" | "string" | "bool";
88
min_value?: number;
99
description: string;
1010
required: boolean;
@@ -15,7 +15,7 @@ export interface SettingCategory {
1515
settings: Setting[];
1616
}
1717

18-
export type SettingsUpdatePayload = Record<string, string | number>;
18+
export type SettingsUpdatePayload = Record<string, string | number | boolean>;
1919

2020
export const settingsApi = {
2121
async getSettings(): Promise<SettingCategory[]> {

web/src/components/keys/GroupFormModal.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
NInputNumber,
1515
NModal,
1616
NSelect,
17+
NSwitch,
1718
NTooltip,
1819
useMessage,
1920
type FormRules,
@@ -34,7 +35,7 @@ interface Emits {
3435
// 配置项类型
3536
interface ConfigItem {
3637
key: string;
37-
value: number | string;
38+
value: number | string | boolean;
3839
}
3940
4041
// Header规则类型
@@ -65,7 +66,7 @@ interface GroupFormData {
6566
test_model: string;
6667
validation_endpoint: string;
6768
param_overrides: string;
68-
config: Record<string, number | string>;
69+
config: Record<string, number | string | boolean>;
6970
configItems: ConfigItem[];
7071
header_rules: HeaderRuleItem[];
7172
proxy_keys: string;
@@ -456,7 +457,7 @@ async function handleSubmit() {
456457
}
457458
458459
// 将configItems转换为config对象
459-
const config: Record<string, number | string> = {};
460+
const config: Record<string, number | string | boolean> = {};
460461
formData.configItems.forEach((item: ConfigItem) => {
461462
if (item.key && item.key.trim()) {
462463
const option = configOptions.value.find(opt => opt.key === item.key);
@@ -861,6 +862,11 @@ async function handleSubmit() {
861862
:precision="0"
862863
style="width: 100%"
863864
/>
865+
<n-switch
866+
v-else-if="typeof configItem.value === 'boolean'"
867+
v-model:value="configItem.value"
868+
size="small"
869+
/>
864870
<n-input v-else v-model:value="configItem.value" placeholder="参数值" />
865871
</template>
866872
{{ getConfigOption(configItem.key)?.description || "设置此配置项的值" }}

0 commit comments

Comments
 (0)