Compare commits

..

No commits in common. "main" and "v1.5.18" have entirely different histories.

10 changed files with 906 additions and 142 deletions

883
.log.meta.json Normal file
View File

@ -0,0 +1,883 @@
{
"debug": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Debug",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"discover": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "App",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 7,
"Name": "Method",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 8,
"Name": "Path",
"KeyName": "",
"AttachBefore": false,
"Color": "blue",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 9,
"Name": "Node",
"KeyName": "",
"AttachBefore": false,
"Color": "yellow",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 10,
"Name": "Attempts",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 11,
"Name": "UsedTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "%.2fms",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 12,
"Name": "Error",
"KeyName": "",
"AttachBefore": false,
"Color": "red",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 13,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"error": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Error",
"KeyName": "",
"AttachBefore": false,
"Color": "red",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 8,
"Name": "CallStacks",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"info": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Info",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"request": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "ServerId",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 7,
"Name": "App",
"KeyName": "App",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 8,
"Name": "Node",
"KeyName": "",
"AttachBefore": true,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 9,
"Name": "FromApp",
"KeyName": "From",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 10,
"Name": "FromNode",
"KeyName": "",
"AttachBefore": true,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 11,
"Name": "ClientIp",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 12,
"Name": "ClientAppName",
"KeyName": "Client",
"AttachBefore": true,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 13,
"Name": "ClientAppVersion",
"KeyName": "",
"AttachBefore": true,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 14,
"Name": "UserId",
"KeyName": "User",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 15,
"Name": "DeviceId",
"KeyName": "Device",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 16,
"Name": "SessionId",
"KeyName": "Session",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 17,
"Name": "Host",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 18,
"Name": "Method",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 19,
"Name": "Path",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 20,
"Name": "Scheme",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 21,
"Name": "Proto",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 22,
"Name": "AuthLevel",
"KeyName": "",
"AttachBefore": false,
"Color": "green",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 23,
"Name": "Priority",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 24,
"Name": "RequestData",
"KeyName": "Request",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 25,
"Name": "RequestHeaders",
"KeyName": "Headers",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 26,
"Name": "UsedTime",
"KeyName": "",
"AttachBefore": false,
"Color": "green",
"Format": "",
"Precision": 6,
"WithoutKey": false,
"Hide": false
},
{
"Index": 27,
"Name": "ResponseCode",
"KeyName": "Status",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 28,
"Name": "ResponseDataLength",
"KeyName": "ContentLength",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 29,
"Name": "ResponseData",
"KeyName": "Response",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 30,
"Name": "ResponseHeaders",
"KeyName": "Headers",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 31,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
],
"warning": [
{
"Index": 0,
"Name": "LogName",
"KeyName": "",
"AttachBefore": false,
"Color": "cyan",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 1,
"Name": "LogType",
"KeyName": "",
"AttachBefore": false,
"Color": "magenta",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 2,
"Name": "LogTime",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "time",
"Precision": 0,
"WithoutKey": false,
"Hide": false
},
{
"Index": 3,
"Name": "TraceId",
"KeyName": "",
"AttachBefore": false,
"Color": "gray",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 4,
"Name": "Image",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 5,
"Name": "Server",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": true
},
{
"Index": 6,
"Name": "Warning",
"KeyName": "",
"AttachBefore": false,
"Color": "yellow",
"Format": "",
"Precision": 0,
"WithoutKey": true,
"Hide": false
},
{
"Index": 7,
"Name": "Extra",
"KeyName": "",
"AttachBefore": false,
"Color": "",
"Format": "",
"Precision": 0,
"WithoutKey": false,
"Hide": false
}
]
}

View File

@ -1,34 +1,5 @@
# CHANGELOG - go/service # CHANGELOG - go/service
## v1.5.20 (2026-06-22)
- **Client Key 应答头条件化**:
- `Device-Id` / `Session-Id` 仅当请求头未携带时才写入应答头,避免客户端已持有 ID 时重复返回。
- 静态文件和 WebSocket 升级应答中不再写入 `Device-Id` / `Session-Id` 头,仅通过 Cookie 维护身份(浏览器 WebSocket API 不支持自定义请求头)。
- **Client App 头命名规范化**:
- 客户端上报键名从 `AppName`/`AppVersion` 改为 `App-Name`/`App-Version`(与 `Device-Id`/`Session-Id` 一致使用破折号)。
- **Breaking**: 旧版客户端需同步修改请求头名称。
- **配置字段重命名**:
- `NoLogHeaders``NoLogRequestHeaders`(请求头排除列表)。
- `NoLogOutputFields``NoLogResponseFields`(响应体字段排除)。
- 新增 `NoLogResponseHeaders`(响应头排除列表,用户可追加自定义字段)。
- **Breaking**: 使用了旧字段名的配置需同步修改。
- **动态排除列表**:
- `NoLogRequestHeaders` 默认值动态构建(包含内部标准头 + 当前配置的 client key 名),用户只需追加自己关心的额外字段。
- 移除 `init()` 中的硬编码默认值。
- **Cookie 头智能过滤**:
- `sanitizeLogHeaders``Cookie` 头不再整体排除,而是解析 Cookie 内容,仅剔除 key 命中排除列表的键值对(如 `Device-Id`/`Session-Id`),保留业务 Cookie。
- 排除列表同时适用于 Header 名匹配和 Cookie key 匹配。
- **日志应答头排除**:
- 响应头日志捕获改为使用 `effectiveNoLogResponseHeaders()`,替代之前的硬编码空字符串。
## v1.5.19 (2026-06-22)
- **依赖更新**:
- 升级依赖 `id``v1.5.6``redis``v1.5.10`
## v1.5.18 (2026-06-21)
- **依赖更新**:
- 升级依赖 `id``v1.5.5``redis``v1.5.9`(同步修复旧版本 ID 未打乱的 Bug
## v1.5.17 (2026-06-21) ## v1.5.17 (2026-06-21)
- **Session 增强**: - **Session 增强**:
- `Save` 改为可变参数 `Save(args ...map[string]any) error`,支持传入 map 批量设置后保存。 - `Save` 改为可变参数 `Save(args ...map[string]any) error`,支持传入 map 批量设置后保存。

10
TEST.md
View File

@ -1,9 +1,9 @@
# Service Module Test Report # Service Module Test Report
## 性能测试 (Benchmark) ## 性能测试 (Benchmark)
- 测试日期: 2026-06-22 - 测试日期: 2026-06-21
- 版本: v1.5.20 - 版本: v1.0.4
- 指标: `BenchmarkRouting`: **5394 ns/op** - 指标: `BenchmarkRouting`: 2791 ns/op
- 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz - 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
## 单元测试覆盖 (Unit Test) ## 单元测试覆盖 (Unit Test)
@ -27,9 +27,7 @@
- [x] `TestSanitizeScalars` ~ `TestSanitizeMixedSlice`: 日志脱敏 10 个测试(标量/对象/数组/嵌套/预算/Unicode - [x] `TestSanitizeScalars` ~ `TestSanitizeMixedSlice`: 日志脱敏 10 个测试(标量/对象/数组/嵌套/预算/Unicode
- [x] `TestSessionLogic`: Session Save/Load/Remove 及 AuthFuncs - [x] `TestSessionLogic`: Session Save/Load/Remove 及 AuthFuncs
- [x] `TestSessionInjection`: Session HTTP 注入流程 - [x] `TestSessionInjection`: Session HTTP 注入流程
- [x] Logging Filters: NoLogInput/NoLogOutput/NoLogAllHeaders/NoLogGets/NoLogRequestHeaders/NoLogResponseHeaders/NoLogResponseFields - [x] Logging Filters: NoLogInput/NoLogOutput/NoLogAllHeaders/NoLogGets/NoLogHeaders
- [x] Client Keys: Device-Id/Session-Id 应答头条件化(请求有则不应答)、静态文件/WebSocket 仅 Cookie
- [x] Cookie 头智能过滤: 排除列表中 key 从 Cookie 内容中剔除,保留业务 Cookie
- [x] Response Body: 200 响应和 dev 模式下 keepBody 捕获 - [x] Response Body: 200 响应和 dev 模式下 keepBody 捕获
## 基础设施对齐验证 ## 基础设施对齐验证

View File

@ -26,12 +26,11 @@ type ServiceConfig struct {
NoLogInput bool // 不记录请求输入 NoLogInput bool // 不记录请求输入
NoLogOutput bool // 不记录响应输出 NoLogOutput bool // 不记录响应输出
NoLogAllHeaders bool // 不记录所有请求/响应头 NoLogAllHeaders bool // 不记录所有请求/响应头
NoLogRequestHeaders string // 不记录请求头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔 NoLogHeaders string // 不记录请求头中包含的这些字段,多个字段用逗号分隔
NoLogResponseHeaders string // 不记录响应头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
LogInputObjectNum int // 请求对象中最多记录的 key 数 LogInputObjectNum int // 请求对象中最多记录的 key 数
LogInputArrayNum int // 请求数组中最多记录的元素数 LogInputArrayNum int // 请求数组中最多记录的元素数
LogInputFieldSize int // 请求单个字段的字符串截断长度 LogInputFieldSize int // 请求单个字段的字符串截断长度
NoLogResponseFields string // 不记录响应字段中包含的这些字段 NoLogOutputFields string // 不记录响应字段中包含的这些字段
LogOutputObjectNum int // 响应对象中最多记录的 key 数 LogOutputObjectNum int // 响应对象中最多记录的 key 数
LogOutputArrayNum int // 响应数组中最多记录的元素数 LogOutputArrayNum int // 响应数组中最多记录的元素数
LogOutputFieldSize int // 响应单个字段的字符串截断长度 LogOutputFieldSize int // 响应单个字段的字符串截断长度

4
go.mod
View File

@ -8,10 +8,10 @@ require (
apigo.cc/go/discover v1.5.3 apigo.cc/go/discover v1.5.3
apigo.cc/go/file v1.5.5 apigo.cc/go/file v1.5.5
apigo.cc/go/http v1.5.3 apigo.cc/go/http v1.5.3
apigo.cc/go/id v1.5.6 apigo.cc/go/id v1.5.5
apigo.cc/go/jsmod v1.5.3 apigo.cc/go/jsmod v1.5.3
apigo.cc/go/log v1.5.8 apigo.cc/go/log v1.5.8
apigo.cc/go/redis v1.5.10 apigo.cc/go/redis v1.5.9
apigo.cc/go/safe v1.5.2 apigo.cc/go/safe v1.5.2
apigo.cc/go/starter v1.5.5 apigo.cc/go/starter v1.5.5
apigo.cc/go/timer v1.5.0 apigo.cc/go/timer v1.5.0

View File

@ -75,13 +75,13 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 请求头 // 请求头
var reqHeaders map[string]string var reqHeaders map[string]string
if !ws.Config.NoLogAllHeaders { if !ws.Config.NoLogAllHeaders {
reqHeaders = sanitizeLogHeaders(r.Header, ws.effectiveNoLogRequestHeaders()) reqHeaders = sanitizeLogHeaders(r.Header, ws.Config.NoLogHeaders)
} }
// 响应头 // 响应头
var respHeaders map[string]string var respHeaders map[string]string
if !ws.Config.NoLogAllHeaders { if !ws.Config.NoLogAllHeaders {
respHeaders = sanitizeLogHeaders(response.Header().H, ws.effectiveNoLogResponseHeaders()) respHeaders = sanitizeLogHeaders(response.Header().H, "")
} }
// 请求输入脱敏 // 请求输入脱敏
@ -434,11 +434,8 @@ func outputResult(response *Response, result any) {
func (ws *WebServer) handleClientKeys(request *Request, response *Response) { func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
// SessionId // SessionId
if ws.usedSessionIdKey != "" { if ws.usedSessionIdKey != "" {
hasRequestSessionId := false
sessionId := request.Header().Get(ws.usedSessionIdKey) sessionId := request.Header().Get(ws.usedSessionIdKey)
if sessionId != "" { if sessionId == "" && !ws.Config.SessionWithoutCookie {
hasRequestSessionId = true
} else if !ws.Config.SessionWithoutCookie {
if ck := request.GetCookie(ws.usedSessionIdKey); ck != nil { if ck := request.GetCookie(ws.usedSessionIdKey); ck != nil {
sessionId = ck.Value sessionId = ck.Value
} }
@ -459,18 +456,13 @@ func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
} }
} }
request.Request.Header.Set(discover.HeaderSessionID, sessionId) request.Request.Header.Set(discover.HeaderSessionID, sessionId)
if !hasRequestSessionId { response.Header().Set(ws.usedSessionIdKey, sessionId)
response.Header().Set(ws.usedSessionIdKey, sessionId)
}
} }
// DeviceId // DeviceId
if ws.usedDeviceIdKey != "" { if ws.usedDeviceIdKey != "" {
hasRequestDeviceId := false
deviceId := request.Header().Get(ws.usedDeviceIdKey) deviceId := request.Header().Get(ws.usedDeviceIdKey)
if deviceId != "" { if deviceId == "" && !ws.Config.DeviceWithoutCookie {
hasRequestDeviceId = true
} else if !ws.Config.DeviceWithoutCookie {
if ck := request.GetCookie(ws.usedDeviceIdKey); ck != nil { if ck := request.GetCookie(ws.usedDeviceIdKey); ck != nil {
deviceId = ck.Value deviceId = ck.Value
} }
@ -488,41 +480,16 @@ func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
} }
} }
request.Request.Header.Set(discover.HeaderDeviceID, deviceId) request.Request.Header.Set(discover.HeaderDeviceID, deviceId)
if !hasRequestDeviceId { response.Header().Set(ws.usedDeviceIdKey, deviceId)
response.Header().Set(ws.usedDeviceIdKey, deviceId)
}
} }
// App-Name / App-Version客户端上报注入内部标准头供下游微服务使用 // AppName / AppVersion客户端上报注入内部标准头供下游微服务使用
if ws.usedClientAppKey != "" { if ws.usedClientAppKey != "" {
if appName := request.Header().Get(ws.usedClientAppKey + "-Name"); appName != "" { if appName := request.Header().Get(ws.usedClientAppKey + "Name"); appName != "" {
request.Request.Header.Set(discover.HeaderClientAppName, appName) request.Request.Header.Set(discover.HeaderClientAppName, appName)
} }
if appVersion := request.Header().Get(ws.usedClientAppKey + "-Version"); appVersion != "" { if appVersion := request.Header().Get(ws.usedClientAppKey + "Version"); appVersion != "" {
request.Request.Header.Set(discover.HeaderClientAppVersion, appVersion) request.Request.Header.Set(discover.HeaderClientAppVersion, appVersion)
} }
} }
} }
// effectiveNoLogRequestHeaders 返回请求头排除列表(动态默认值 + 用户配置追加)
func (ws *WebServer) effectiveNoLogRequestHeaders() string {
parts := []string{"X-Request-Id", "X-Device-Id", "X-Session-Id"}
if ws.usedDeviceIdKey != "" {
parts = append(parts, ws.usedDeviceIdKey)
}
if ws.usedSessionIdKey != "" {
parts = append(parts, ws.usedSessionIdKey)
}
if ws.usedClientAppKey != "" {
parts = append(parts, ws.usedClientAppKey+"-Name", ws.usedClientAppKey+"-Version")
}
if ws.Config.NoLogRequestHeaders != "" {
parts = append(parts, ws.Config.NoLogRequestHeaders)
}
return strings.Join(parts, ",")
}
// effectiveNoLogResponseHeaders 返回响应头排除列表(用户配置追加)
func (ws *WebServer) effectiveNoLogResponseHeaders() string {
return ws.Config.NoLogResponseHeaders
}

View File

@ -137,21 +137,14 @@ func sanitizeSliceContent(s []any, opts sanitizeOpts, budget *int) []any {
return result return result
} }
// sanitizeLogHeaders 过滤请求/响应头,排除 noLogHeaders 中指定的字段。 // sanitizeLogHeaders 过滤请求/响应头,排除 NoLogHeaders 中指定的字段
// 对于 Cookie 头,不整体排除,而是从 Cookie 内容中剔除 key 命中排除列表的键值对。
func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string { func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string {
result := make(map[string]string) result := make(map[string]string)
excludes := splitAndTrim(noLogHeaders) excludes := strings.Split(noLogHeaders, ",")
for k, v := range h { for k, v := range h {
if k == "Cookie" && len(excludes) > 0 {
if filtered := sanitizeCookieValue(v, excludes); filtered != "" {
result[k] = filtered
}
continue
}
skip := false skip := false
for _, ex := range excludes { for _, ex := range excludes {
if strings.EqualFold(k, ex) { if ex != "" && strings.EqualFold(k, strings.TrimSpace(ex)) {
skip = true skip = true
break break
} }
@ -163,52 +156,14 @@ func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string {
return result return result
} }
// sanitizeCookieValue 从 Cookie 原始值中剔除 key 命中排除列表的键值对。
// 输入为原始 Cookie 头值(可能多个 key=value 以 "; " 或 ";" 分隔)。
func sanitizeCookieValue(values []string, excludes []string) string {
raw := strings.Join(values, "; ")
pairs := strings.Split(raw, ";")
var kept []string
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
key, _, _ := strings.Cut(pair, "=")
key = strings.TrimSpace(key)
excluded := false
for _, ex := range excludes {
if strings.EqualFold(key, ex) {
excluded = true
break
}
}
if !excluded {
kept = append(kept, pair)
}
}
return strings.Join(kept, "; ")
}
func splitAndTrim(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
return parts
}
// sanitizeRespBody 对响应体进行脱敏:尝试 JSON 解析后走对象脱敏,失败则按字符串截断 // sanitizeRespBody 对响应体进行脱敏:尝试 JSON 解析后走对象脱敏,失败则按字符串截断
func sanitizeRespBody(body []byte, cfg *ServiceConfig) any { func sanitizeRespBody(body []byte, cfg *ServiceConfig) any {
// 尝试解析为 JSON 对象 // 尝试解析为 JSON 对象
var parsed any var parsed any
if err := cast.UnmarshalJSON(body, &parsed); err == nil && parsed != nil { if err := cast.UnmarshalJSON(body, &parsed); err == nil && parsed != nil {
// 排除敏感字段 // 排除敏感字段
if cfg.NoLogResponseFields != "" { if cfg.NoLogOutputFields != "" {
parsed = stripFields(parsed, cfg.NoLogResponseFields) parsed = stripFields(parsed, cfg.NoLogOutputFields)
} }
return sanitizeLogData(parsed, sanitizeOpts{ return sanitizeLogData(parsed, sanitizeOpts{
maxSize: 200, maxSize: 200,

View File

@ -111,7 +111,7 @@ var DefaultServer = NewWebServer()
var Config = &DefaultServer.Config var Config = &DefaultServer.Config
func init() { func init() {
// NoLogRequestHeaders / NoLogResponseHeaders 的默认值在日志捕获时动态构建 Config.NoLogHeaders = "X-Request-Id,X-Device-Id,X-Session-Id,Cookie,Device-Id,Session-Id"
Config.LogInputObjectNum = 10 Config.LogInputObjectNum = 10
Config.LogInputArrayNum = 5 Config.LogInputArrayNum = 5
Config.LogInputFieldSize = 20 Config.LogInputFieldSize = 20

View File

@ -131,10 +131,6 @@ func (ws *WebServer) processStatic(requestPath string, request *Request, respons
return false return false
} }
// 静态文件通过 Cookie 维护 ID不应答 Device-Id / Session-Id 头
response.Header().Del(ws.usedDeviceIdKey)
response.Header().Del(ws.usedSessionIdKey)
// 检查 304 // 检查 304
if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" { if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" {
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil { if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {

View File

@ -78,11 +78,6 @@ func Upgrade(response *Response, request *Request) (*WebSocketConn, error) {
} }
func (ws *WebServer) doWebsocketService(wsc *websocketServiceType, request *Request, response *Response, logger *log.Logger, object any) { func (ws *WebServer) doWebsocketService(wsc *websocketServiceType, request *Request, response *Response, logger *log.Logger, object any) {
// WebSocket 浏览器 API 不支持自定义请求头,只能通过 Cookie 传递身份标识,
// 因此不应在升级应答中返回 Device-Id / Session-Id 头。
response.Header().Del(ws.usedDeviceIdKey)
response.Header().Del(ws.usedSessionIdKey)
wsConn, err := Upgrade(response, request) wsConn, err := Upgrade(response, request)
if err != nil { if err != nil {
logger.Error("websocket upgrade failed", "error", err.Error()) logger.Error("websocket upgrade failed", "error", err.Error())