Compare commits
No commits in common. "main" and "v1.5.15" have entirely different histories.
883
.log.meta.json
Normal file
883
.log.meta.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
50
CHANGELOG.md
50
CHANGELOG.md
@ -1,55 +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)
|
|
||||||
- **Session 增强**:
|
|
||||||
- `Save` 改为可变参数 `Save(args ...map[string]any) error`,支持传入 map 批量设置后保存。
|
|
||||||
- 新增 `Load(keys []string) map[string]any`,支持批量读取,空参数返回全部数据。
|
|
||||||
- `Remove` 改为可变参数 `Remove(keys ...string)`,支持一次删除多个 key。
|
|
||||||
- **日志脱敏引擎**:
|
|
||||||
- 新增 `sanitizeLogData` 递归脱敏函数,基于 size 预算、字符串截断、数组/对象元素限制构建日志安全对象,不影响原始数据。
|
|
||||||
- 新增 `sanitizeLogHeaders` 统一处理请求/响应头过滤。
|
|
||||||
- 新增 `sanitizeRespBody` 自动 JSON 解析后走对象脱敏,非 JSON 按字符串截断。
|
|
||||||
- **日志配置新增**:
|
|
||||||
- `NoLogInput`/`NoLogOutput`/`NoLogAllHeaders` — 高并发加速开关。
|
|
||||||
- `LogInputObjectNum`/`LogOutputObjectNum` — 对象最多记录 key 数(默认 10/5)。
|
|
||||||
- 各配置默认值: fieldSize=20, arrayNum(In=5/Out=3), maxSize=200, NoLogHeaders 默认过滤内部头。
|
|
||||||
- **响应体捕获修复**:
|
|
||||||
- `response.Write` 改为始终调用 `keepBody` 缓冲 body(限制大小 LogOutputMaxSize),修复 200 响应无日志输出的问题。
|
|
||||||
- `outputResult` 先调 `keepBody` 再分路径写出,修复 dev 模式(hasOutFilter)下 PhysicalWrite 绕过 body 缓冲的问题。
|
|
||||||
- **客户端 Key 默认值**: `Session-ID`/`Device-ID`/`App`,配合 HTTP header canonicalize 显示为 `Session-Id`/`Device-Id`。
|
|
||||||
- **调试清理**: 移除 handler.go 中 `fmt.Println`/`shell.BMagenta` 调试残留。
|
|
||||||
- **日志格式优化**: RequestHeaders 颜色 blue,ResponseDataLength key 为 Size,ResponseData/ResponseHeaders 颜色 yellow。
|
|
||||||
- **依赖更新**: 升级 `js` 至 `v1.5.6`。
|
|
||||||
|
|
||||||
## v1.5.15 (2026-06-21)
|
## v1.5.15 (2026-06-21)
|
||||||
- **错误堆栈重构**:
|
- **错误堆栈重构**:
|
||||||
- 重构 `js_export.go`,将匿名占位工厂函数改写为包级具名函数。
|
- 重构 `js_export.go`,将匿名占位工厂函数改写为包级具名函数。
|
||||||
|
|||||||
14
TEST.md
14
TEST.md
@ -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)
|
||||||
@ -24,13 +24,7 @@
|
|||||||
- [x] `TestGetDefaultName`: 自动应用名识别
|
- [x] `TestGetDefaultName`: 自动应用名识别
|
||||||
- [x] `TestGetServerIp`: 自动 IP 探测
|
- [x] `TestGetServerIp`: 自动 IP 探测
|
||||||
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
||||||
- [x] `TestSanitizeScalars` ~ `TestSanitizeMixedSlice`: 日志脱敏 10 个测试(标量/对象/数组/嵌套/预算/Unicode)
|
- [x] **Logging Filters**: 已手动验证 `NoLogGets`, `NoLogHeaders` 等过滤逻辑。
|
||||||
- [x] `TestSessionLogic`: Session Save/Load/Remove 及 AuthFuncs
|
|
||||||
- [x] `TestSessionInjection`: Session HTTP 注入流程
|
|
||||||
- [x] Logging Filters: NoLogInput/NoLogOutput/NoLogAllHeaders/NoLogGets/NoLogRequestHeaders/NoLogResponseHeaders/NoLogResponseFields
|
|
||||||
- [x] Client Keys: Device-Id/Session-Id 应答头条件化(请求有则不应答)、静态文件/WebSocket 仅 Cookie
|
|
||||||
- [x] Cookie 头智能过滤: 排除列表中 key 从 Cookie 内容中剔除,保留业务 Cookie
|
|
||||||
- [x] Response Body: 200 响应和 dev 模式下 keepBody 捕获
|
|
||||||
|
|
||||||
## 基础设施对齐验证
|
## 基础设施对齐验证
|
||||||
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
||||||
|
|||||||
19
config.go
19
config.go
@ -23,19 +23,12 @@ type ServiceConfig struct {
|
|||||||
Listen string // 监听端口(|隔开多个监听)(,隔开多个选项),例如 80,http|443|443:h2|127.0.0.1:8080,h2c
|
Listen string // 监听端口(|隔开多个监听)(,隔开多个选项),例如 80,http|443|443:h2|127.0.0.1:8080,h2c
|
||||||
SSL map[string]*CertSet // SSL 证书配置,key 为域名
|
SSL map[string]*CertSet // SSL 证书配置,key 为域名
|
||||||
NoLogGets bool // 不记录 GET 请求的日志
|
NoLogGets bool // 不记录 GET 请求的日志
|
||||||
NoLogInput bool // 不记录请求输入
|
NoLogHeaders string // 不记录请求头中包含的这些字段,多个字段用逗号分隔
|
||||||
NoLogOutput bool // 不记录响应输出
|
LogInputArrayNum int // 请求字段中容器类型在日志打印个数限制
|
||||||
NoLogAllHeaders bool // 不记录所有请求/响应头
|
LogInputFieldSize int // 请求字段中单个字段在日志打印长度限制
|
||||||
NoLogRequestHeaders string // 不记录请求头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
|
NoLogOutputFields string // 不记录响应字段中包含的这些字段
|
||||||
NoLogResponseHeaders string // 不记录响应头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
|
LogOutputArrayNum int // 响应字段中容器类型在日志打印个数限制
|
||||||
LogInputObjectNum int // 请求对象中最多记录的 key 数
|
LogOutputFieldSize int // 响应字段中单个字段在日志打印长度限制
|
||||||
LogInputArrayNum int // 请求数组中最多记录的元素数
|
|
||||||
LogInputFieldSize int // 请求单个字段的字符串截断长度
|
|
||||||
NoLogResponseFields string // 不记录响应字段中包含的这些字段
|
|
||||||
LogOutputObjectNum int // 响应对象中最多记录的 key 数
|
|
||||||
LogOutputArrayNum int // 响应数组中最多记录的元素数
|
|
||||||
LogOutputFieldSize int // 响应单个字段的字符串截断长度
|
|
||||||
LogOutputMaxSize int // 非对象响应内容的最大记录长度
|
|
||||||
Compress bool // 是否启用压缩
|
Compress bool // 是否启用压缩
|
||||||
CompressMinSize int // 启用压缩的最小长度
|
CompressMinSize int // 启用压缩的最小长度
|
||||||
CompressMaxSize int // 启用压缩的最大长度
|
CompressMaxSize int // 启用压缩的最大长度
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -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.4
|
||||||
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.6
|
||||||
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
|
||||||
@ -24,7 +24,7 @@ require (
|
|||||||
apigo.cc/go/crypto v1.5.3 // indirect
|
apigo.cc/go/crypto v1.5.3 // indirect
|
||||||
apigo.cc/go/encoding v1.5.4 // indirect
|
apigo.cc/go/encoding v1.5.4 // indirect
|
||||||
apigo.cc/go/rand v1.5.3 // indirect
|
apigo.cc/go/rand v1.5.3 // indirect
|
||||||
apigo.cc/go/shell v1.5.4 // indirect
|
apigo.cc/go/shell v1.5.3 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||||
|
|||||||
12
go.sum
12
go.sum
@ -12,20 +12,20 @@ apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
|
|||||||
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
|
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
|
||||||
apigo.cc/go/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ=
|
apigo.cc/go/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ=
|
||||||
apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
|
apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
|
||||||
apigo.cc/go/id v1.5.5 h1:fQXfb2WZ4hEtzXkpb9w9o8AOcTZ44fYQfTV6iZ49l8o=
|
apigo.cc/go/id v1.5.4 h1:D1Zx9gEZhOgdTgZ4SdmPImhpc9xGiOA33Y+j2MkstzQ=
|
||||||
apigo.cc/go/id v1.5.5/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
|
apigo.cc/go/id v1.5.4/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
|
||||||
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
||||||
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||||
apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0=
|
apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0=
|
||||||
apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE=
|
apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE=
|
||||||
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
|
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
|
||||||
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
||||||
apigo.cc/go/redis v1.5.9 h1:h77XPjQWcIDgxrtwjtS/gwT6PS5nSk6/x785t0OMlbY=
|
apigo.cc/go/redis v1.5.6 h1:Lzo8M2binfqdQdVVp31Z/Max4qT8D82QdZjLlLQsrIY=
|
||||||
apigo.cc/go/redis v1.5.9/go.mod h1:YZRDLA3gWw9LAc4Z/4GrDEy8eMDMdDPDA0iqSEAu1fE=
|
apigo.cc/go/redis v1.5.6/go.mod h1:HmqSh2Ll7/b2zFXDi2Ap13YOuMCVniuZNbwtxkbIYII=
|
||||||
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
|
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
|
||||||
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
|
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
|
||||||
apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc=
|
apigo.cc/go/shell v1.5.3 h1:pI+u12sy6upoygq+1XXqUlvUboBfH4Q52jRpoJFv56A=
|
||||||
apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
apigo.cc/go/shell v1.5.3/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
||||||
apigo.cc/go/starter v1.5.5 h1:4ST02o4qP8IIekxtd9Jhx5RHTrSGXtVQUguSIXV0iWc=
|
apigo.cc/go/starter v1.5.5 h1:4ST02o4qP8IIekxtd9Jhx5RHTrSGXtVQUguSIXV0iWc=
|
||||||
apigo.cc/go/starter v1.5.5/go.mod h1:WAGhdtmZdpP1Jn/z0pCqHwpTbqqaFhm5OqH7QVtcanY=
|
apigo.cc/go/starter v1.5.5/go.mod h1:WAGhdtmZdpP1Jn/z0pCqHwpTbqqaFhm5OqH7QVtcanY=
|
||||||
apigo.cc/go/timer v1.5.0 h1:iPo/IQn+iuhBRI1/MR1txwZnamef/RBBfOiIlBiqkgk=
|
apigo.cc/go/timer v1.5.0 h1:iPo/IQn+iuhBRI1/MR1txwZnamef/RBBfOiIlBiqkgk=
|
||||||
|
|||||||
113
handler.go
113
handler.go
@ -1,6 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
"apigo.cc/go/discover"
|
||||||
|
"apigo.cc/go/log"
|
||||||
|
"apigo.cc/go/timer"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -9,11 +13,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
"apigo.cc/go/discover"
|
|
||||||
"apigo.cc/go/log"
|
|
||||||
"apigo.cc/go/timer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RouteHandler struct {
|
type RouteHandler struct {
|
||||||
@ -72,33 +71,43 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
usedTime := float32(tracker.Stop().Seconds())
|
usedTime := float32(tracker.Stop().Seconds())
|
||||||
|
|
||||||
// 请求头
|
// 过滤请求头
|
||||||
var reqHeaders map[string]string
|
reqHeaders := make(map[string]string)
|
||||||
if !ws.Config.NoLogAllHeaders {
|
noLogHeaders := strings.Split(ws.Config.NoLogHeaders, ",")
|
||||||
reqHeaders = sanitizeLogHeaders(r.Header, ws.effectiveNoLogRequestHeaders())
|
for k, v := range r.Header {
|
||||||
|
skip := false
|
||||||
|
for _, nl := range noLogHeaders {
|
||||||
|
if nl != "" && strings.EqualFold(k, strings.TrimSpace(nl)) {
|
||||||
|
skip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skip {
|
||||||
|
reqHeaders[k] = strings.Join(v, ", ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应头
|
// 过滤响应头
|
||||||
var respHeaders map[string]string
|
respHeaders := make(map[string]string)
|
||||||
if !ws.Config.NoLogAllHeaders {
|
for k, v := range response.Header().H {
|
||||||
respHeaders = sanitizeLogHeaders(response.Header().H, ws.effectiveNoLogResponseHeaders())
|
respHeaders[k] = strings.Join(v, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求输入脱敏
|
// 处理响应内容截断
|
||||||
var reqData any
|
|
||||||
if !ws.Config.NoLogInput && args != nil {
|
|
||||||
reqData = sanitizeLogData(args, sanitizeOpts{
|
|
||||||
maxSize: 200,
|
|
||||||
fieldSize: ws.Config.LogInputFieldSize,
|
|
||||||
arrayNum: ws.Config.LogInputArrayNum,
|
|
||||||
objectNum: ws.Config.LogInputObjectNum,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应输出脱敏
|
|
||||||
var respData any
|
var respData any
|
||||||
if !ws.Config.NoLogOutput && len(response.body) > 0 {
|
if response.Code != 200 {
|
||||||
respData = sanitizeRespBody(response.body, &ws.Config)
|
if len(response.body) < 1024 {
|
||||||
|
respData = string(response.body)
|
||||||
|
} else {
|
||||||
|
respData = string(response.body[:1024]) + "..."
|
||||||
|
}
|
||||||
|
} else if ws.Config.NoLogOutputFields != "" {
|
||||||
|
// 简单的字段过滤逻辑 (如果是 JSON 对象)
|
||||||
|
// 这里可以根据 Config.NoLogOutputFields, LogOutputArrayNum, LogOutputFieldSize 进行更复杂的处理
|
||||||
|
// 暂按字符串截断处理
|
||||||
|
if len(response.body) > 0 {
|
||||||
|
respData = "[content hidden or truncated]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LogRequest(requestLogger, func(entry *RequestLog) {
|
LogRequest(requestLogger, func(entry *RequestLog) {
|
||||||
@ -119,7 +128,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
entry.AuthLevel = authLevel
|
entry.AuthLevel = authLevel
|
||||||
entry.Priority = priority
|
entry.Priority = priority
|
||||||
entry.RequestHeaders = reqHeaders
|
entry.RequestHeaders = reqHeaders
|
||||||
entry.RequestData = reqData
|
entry.RequestData = args
|
||||||
entry.ResponseCode = response.Code
|
entry.ResponseCode = response.Code
|
||||||
entry.UsedTime = usedTime
|
entry.UsedTime = usedTime
|
||||||
entry.ResponseHeaders = respHeaders
|
entry.ResponseHeaders = respHeaders
|
||||||
@ -424,7 +433,6 @@ func outputResult(response *Response, result any) {
|
|||||||
response.Header().Set("Content-Type", contentType)
|
response.Header().Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
response.keepBody(data)
|
|
||||||
if response.server != nil && response.server.hasOutFilter {
|
if response.server != nil && response.server.hasOutFilter {
|
||||||
response.PhysicalWrite(data)
|
response.PhysicalWrite(data)
|
||||||
} else {
|
} else {
|
||||||
@ -434,11 +442,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 +464,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 +488,6 @@ 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(客户端上报,注入内部标准头供下游微服务使用)
|
|
||||||
if ws.usedClientAppKey != "" {
|
|
||||||
if appName := request.Header().Get(ws.usedClientAppKey + "-Name"); appName != "" {
|
|
||||||
request.Request.Header.Set(discover.HeaderClientAppName, appName)
|
|
||||||
}
|
|
||||||
if appVersion := request.Header().Get(ws.usedClientAppKey + "-Version"); 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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func jsUpgrade(response *Response, request *Request) (*WebSocketConn, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// jsUploadFile 包装 UploadFile 以隐藏敏感方法(如 Open)
|
// jsUploadFile 包装 UploadFile 以隐藏敏感方法
|
||||||
type jsUploadFile struct {
|
type jsUploadFile struct {
|
||||||
f *UploadFile
|
f *UploadFile
|
||||||
}
|
}
|
||||||
|
|||||||
6
log.go
6
log.go
@ -25,12 +25,12 @@ type RequestLog struct {
|
|||||||
AuthLevel int `log:"pos:22,color:green"`
|
AuthLevel int `log:"pos:22,color:green"`
|
||||||
Priority int `log:"pos:23,hide:true"`
|
Priority int `log:"pos:23,hide:true"`
|
||||||
RequestData any `log:"pos:24,color:cyan,keyname:Request"`
|
RequestData any `log:"pos:24,color:cyan,keyname:Request"`
|
||||||
RequestHeaders map[string]string `log:"pos:25,color:blue,keyname:Headers"`
|
RequestHeaders map[string]string `log:"pos:25,color:cyan,keyname:Headers"`
|
||||||
UsedTime float32 `log:"pos:26,color:green,precision:6"`
|
UsedTime float32 `log:"pos:26,color:green,precision:6"`
|
||||||
ResponseCode int `log:"pos:27,color:magenta,keyname:Status"`
|
ResponseCode int `log:"pos:27,color:magenta,keyname:Status"`
|
||||||
ResponseDataLength uint `log:"pos:28,color:magenta,keyname:Size"`
|
ResponseDataLength uint `log:"pos:28,color:magenta,keyname:ContentLength"`
|
||||||
ResponseData any `log:"pos:29,color:magenta,keyname:Response"`
|
ResponseData any `log:"pos:29,color:magenta,keyname:Response"`
|
||||||
ResponseHeaders map[string]string `log:"pos:30,color:yellow,keyname:Headers"`
|
ResponseHeaders map[string]string `log:"pos:30,color:magenta,keyname:Headers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *RequestLog) Reset() {
|
func (l *RequestLog) Reset() {
|
||||||
|
|||||||
242
log_sanitize.go
242
log_sanitize.go
@ -1,242 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sanitizeOpts 日志脱敏配置
|
|
||||||
type sanitizeOpts struct {
|
|
||||||
maxSize int // 整体尺寸上限(内容字符估算)
|
|
||||||
fieldSize int // 单字符串截断长度
|
|
||||||
arrayNum int // 数组最多保留元素数
|
|
||||||
objectNum int // 对象最多保留 key 数
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeLogData 递归脱敏,返回新建的对象,不影响原始数据
|
|
||||||
func sanitizeLogData(v any, opts sanitizeOpts) any {
|
|
||||||
budget := opts.maxSize
|
|
||||||
return sanitizeRecursive(v, opts, &budget)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeRecursive(v any, opts sanitizeOpts, budget *int) any {
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case string:
|
|
||||||
return sanitizeString(val, opts, budget)
|
|
||||||
case bool:
|
|
||||||
return sanitizeScalar(val, 5, budget)
|
|
||||||
case float64:
|
|
||||||
return sanitizeScalar(val, 8, budget)
|
|
||||||
case map[string]any:
|
|
||||||
return sanitizeMapContent(val, opts, budget)
|
|
||||||
case []any:
|
|
||||||
return sanitizeSliceContent(val, opts, budget)
|
|
||||||
default:
|
|
||||||
// int 等各种数值类型
|
|
||||||
return sanitizeScalar(val, 8, budget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeString(s string, opts sanitizeOpts, budget *int) string {
|
|
||||||
if len([]rune(s)) > opts.fieldSize {
|
|
||||||
s = string([]rune(s)[:opts.fieldSize])
|
|
||||||
}
|
|
||||||
if *budget < len(s) {
|
|
||||||
*budget = 0
|
|
||||||
return s // 即使超出预算也返回截断后的内容
|
|
||||||
}
|
|
||||||
*budget -= len(s)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeScalar(v any, cost int, budget *int) any {
|
|
||||||
if *budget < cost {
|
|
||||||
*budget = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
*budget -= cost
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeMapContent(m map[string]any, opts sanitizeOpts, budget *int) map[string]any {
|
|
||||||
// 空 map 占 2 预算
|
|
||||||
if *budget < 2 {
|
|
||||||
*budget = 0
|
|
||||||
return map[string]any{}
|
|
||||||
}
|
|
||||||
*budget -= 2
|
|
||||||
|
|
||||||
result := make(map[string]any)
|
|
||||||
// 排序 key 保证遍历顺序确定性
|
|
||||||
keys := make([]string, 0, len(m))
|
|
||||||
for k := range m {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for _, k := range keys {
|
|
||||||
if count >= opts.objectNum || *budget <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扣 key 长度
|
|
||||||
keyCost := len(k)
|
|
||||||
if *budget < keyCost {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
budgetBefore := *budget
|
|
||||||
*budget -= keyCost
|
|
||||||
|
|
||||||
// 递归处理 value
|
|
||||||
processed := sanitizeRecursive(m[k], opts, budget)
|
|
||||||
|
|
||||||
// 检查是否因预算不足返回了 nil(仅当原值非 nil 时)
|
|
||||||
if processed == nil && m[k] != nil {
|
|
||||||
*budget = budgetBefore
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
result[k] = processed
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeSliceContent(s []any, opts sanitizeOpts, budget *int) []any {
|
|
||||||
// 空 slice 占 2 预算
|
|
||||||
if *budget < 2 {
|
|
||||||
*budget = 0
|
|
||||||
return []any{}
|
|
||||||
}
|
|
||||||
*budget -= 2
|
|
||||||
|
|
||||||
result := make([]any, 0, min(len(s), opts.arrayNum))
|
|
||||||
count := 0
|
|
||||||
for _, v := range s {
|
|
||||||
if count >= opts.arrayNum || *budget <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 值预算由递归处理
|
|
||||||
processed := sanitizeRecursive(v, opts, budget)
|
|
||||||
if processed == nil && v != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, processed)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeLogHeaders 过滤请求/响应头,排除 noLogHeaders 中指定的字段。
|
|
||||||
// 对于 Cookie 头,不整体排除,而是从 Cookie 内容中剔除 key 命中排除列表的键值对。
|
|
||||||
func sanitizeLogHeaders(h http.Header, noLogHeaders string) map[string]string {
|
|
||||||
result := make(map[string]string)
|
|
||||||
excludes := splitAndTrim(noLogHeaders)
|
|
||||||
for k, v := range h {
|
|
||||||
if k == "Cookie" && len(excludes) > 0 {
|
|
||||||
if filtered := sanitizeCookieValue(v, excludes); filtered != "" {
|
|
||||||
result[k] = filtered
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
skip := false
|
|
||||||
for _, ex := range excludes {
|
|
||||||
if strings.EqualFold(k, ex) {
|
|
||||||
skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !skip {
|
|
||||||
result[k] = strings.Join(v, ", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 解析后走对象脱敏,失败则按字符串截断
|
|
||||||
func sanitizeRespBody(body []byte, cfg *ServiceConfig) any {
|
|
||||||
// 尝试解析为 JSON 对象
|
|
||||||
var parsed any
|
|
||||||
if err := cast.UnmarshalJSON(body, &parsed); err == nil && parsed != nil {
|
|
||||||
// 排除敏感字段
|
|
||||||
if cfg.NoLogResponseFields != "" {
|
|
||||||
parsed = stripFields(parsed, cfg.NoLogResponseFields)
|
|
||||||
}
|
|
||||||
return sanitizeLogData(parsed, sanitizeOpts{
|
|
||||||
maxSize: 200,
|
|
||||||
fieldSize: cfg.LogOutputFieldSize,
|
|
||||||
arrayNum: cfg.LogOutputArrayNum,
|
|
||||||
objectNum: cfg.LogOutputObjectNum,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非 JSON 内容,按字符串截断
|
|
||||||
if len(body) > cfg.LogOutputMaxSize {
|
|
||||||
return string(body[:cfg.LogOutputMaxSize]) + "..."
|
|
||||||
}
|
|
||||||
return string(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stripFields 从对象中删除指定字段(仅处理顶层 map)
|
|
||||||
func stripFields(v any, fields string) any {
|
|
||||||
m, ok := v.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
excludes := strings.Split(fields, ",")
|
|
||||||
for _, f := range excludes {
|
|
||||||
f = strings.TrimSpace(f)
|
|
||||||
if f != "" {
|
|
||||||
delete(m, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@ -1,250 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSanitizeScalars(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input any
|
|
||||||
expected any
|
|
||||||
}{
|
|
||||||
{"nil", nil, nil},
|
|
||||||
{"int", 42, 42},
|
|
||||||
{"float", 3.14, 3.14},
|
|
||||||
{"bool_true", true, true},
|
|
||||||
{"bool_false", false, false},
|
|
||||||
{"string_short", "hello", "hello"},
|
|
||||||
{"string_exact", "12345678901234567890", "12345678901234567890"},
|
|
||||||
{"string_over", "123456789012345678901234567890", "12345678901234567890"},
|
|
||||||
{"string_unicode", "你好世界你好世界你好世界你好世界你好世界你好世界你好世界", "你好世界你好世界你好世界你好世界你好世界"},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := sanitizeLogData(tt.input, opts)
|
|
||||||
if !reflect.DeepEqual(got, tt.expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeMapBasic(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"name": "John",
|
|
||||||
"bio": "A very long biography that should be truncated at twenty",
|
|
||||||
"status": "active",
|
|
||||||
}
|
|
||||||
expected := map[string]any{
|
|
||||||
"name": "John",
|
|
||||||
"bio": "A very long biograph",
|
|
||||||
"status": "active",
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeMapObjectNum(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 3}
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"k1": "v1",
|
|
||||||
"k2": "v2",
|
|
||||||
"k3": "v3",
|
|
||||||
"k4": "v4",
|
|
||||||
"k5": "v5",
|
|
||||||
}
|
|
||||||
expected := map[string]any{
|
|
||||||
"k1": "v1",
|
|
||||||
"k2": "v2",
|
|
||||||
"k3": "v3",
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeSlice(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
input := []any{1, 2, 3, 4, 5, 6, 7}
|
|
||||||
expected := []any{1, 2, 3}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeSliceWithStrings(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
input := []any{
|
|
||||||
"short",
|
|
||||||
"this string is definitely way too long for twenty characters",
|
|
||||||
"ok",
|
|
||||||
"this would be fourth but arrayNum is 3",
|
|
||||||
}
|
|
||||||
expected := []any{
|
|
||||||
"short",
|
|
||||||
"this string is defin",
|
|
||||||
"ok",
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeNested(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"user": map[string]any{
|
|
||||||
"name": "John Doe",
|
|
||||||
"bio": "A very long description that should be truncated at twenty chars",
|
|
||||||
"tags": []any{"go", "javascript-long-name", "rust", "python", "java", "c++"},
|
|
||||||
},
|
|
||||||
"score": 100,
|
|
||||||
}
|
|
||||||
expected := map[string]any{
|
|
||||||
"user": map[string]any{
|
|
||||||
"name": "John Doe",
|
|
||||||
"bio": "A very long descript",
|
|
||||||
"tags": []any{"go", "javascript-long-name", "rust"},
|
|
||||||
},
|
|
||||||
"score": 100,
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeTopLevelArray(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
input := []any{
|
|
||||||
map[string]any{"id": 1, "name": "Alice"},
|
|
||||||
map[string]any{"id": 2, "name": "Bob"},
|
|
||||||
map[string]any{"id": 3, "name": "Charlie"},
|
|
||||||
map[string]any{"id": 4, "name": "Diana"},
|
|
||||||
map[string]any{"id": 5, "name": "Eve"},
|
|
||||||
}
|
|
||||||
expected := []any{
|
|
||||||
map[string]any{"id": 1, "name": "Alice"},
|
|
||||||
map[string]any{"id": 2, "name": "Bob"},
|
|
||||||
map[string]any{"id": 3, "name": "Charlie"},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeBudgetExhausted(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 22, fieldSize: 20, arrayNum: 10, objectNum: 10}
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"a": "1234567890",
|
|
||||||
"b": "abcdefghij",
|
|
||||||
"c": "should-be-dropped",
|
|
||||||
}
|
|
||||||
expected := map[string]any{
|
|
||||||
"a": "1234567890",
|
|
||||||
"b": "abcdefghij",
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeNestedBudgetExhausted(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 30, fieldSize: 20, arrayNum: 10, objectNum: 10}
|
|
||||||
|
|
||||||
input := map[string]any{
|
|
||||||
"small": "hi",
|
|
||||||
"nested": map[string]any{
|
|
||||||
"field1": "1234567890",
|
|
||||||
"field2": "abcdefghij",
|
|
||||||
},
|
|
||||||
"after": "no",
|
|
||||||
}
|
|
||||||
// 按字母序: after(7) → nested(22) → small(被跳过)
|
|
||||||
expected := map[string]any{
|
|
||||||
"after": "no",
|
|
||||||
"nested": map[string]any{
|
|
||||||
"field1": "1234567890",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeEmptyContainers(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 3, objectNum: 10}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input any
|
|
||||||
expected any
|
|
||||||
}{
|
|
||||||
{"empty_map", map[string]any{}, map[string]any{}},
|
|
||||||
{"empty_slice", []any{}, []any{}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := sanitizeLogData(tt.input, opts)
|
|
||||||
if !reflect.DeepEqual(got, tt.expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSanitizeMixedSlice(t *testing.T) {
|
|
||||||
opts := sanitizeOpts{maxSize: 200, fieldSize: 20, arrayNum: 10, objectNum: 10}
|
|
||||||
|
|
||||||
input := []any{
|
|
||||||
"ok",
|
|
||||||
42,
|
|
||||||
true,
|
|
||||||
nil,
|
|
||||||
map[string]any{"nested": "value"},
|
|
||||||
"this one is too long and will be trimmed to twenty chars",
|
|
||||||
nil,
|
|
||||||
}
|
|
||||||
expected := []any{
|
|
||||||
"ok",
|
|
||||||
42,
|
|
||||||
true,
|
|
||||||
nil,
|
|
||||||
map[string]any{"nested": "value"},
|
|
||||||
"this one is too long",
|
|
||||||
nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
got := sanitizeLogData(input, opts)
|
|
||||||
if !reflect.DeepEqual(got, expected) {
|
|
||||||
t.Errorf("got %v, want %v", got, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
response.go
22
response.go
@ -67,8 +67,10 @@ func (r *Response) Write(bytes []byte) (int, error) {
|
|||||||
return len(bytes), nil
|
return len(bytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓冲 body 用于日志记录
|
// 即使没有过滤器,非 200 状态码也进行缓冲以便日志记录
|
||||||
r.keepBody(bytes)
|
if r.Code != http.StatusOK {
|
||||||
|
r.body = append(r.body, bytes...)
|
||||||
|
}
|
||||||
|
|
||||||
if r.ProxyHeader != nil {
|
if r.ProxyHeader != nil {
|
||||||
r.copyProxyHeader()
|
r.copyProxyHeader()
|
||||||
@ -80,22 +82,6 @@ func (r *Response) Write(bytes []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// keepBody 缓冲数据用于日志记录,限制大小防止内存问题
|
|
||||||
func (r *Response) keepBody(bytes []byte) {
|
|
||||||
maxBuf := 200
|
|
||||||
if r.server != nil && r.server.Config.LogOutputMaxSize > 0 {
|
|
||||||
maxBuf = r.server.Config.LogOutputMaxSize
|
|
||||||
}
|
|
||||||
if len(r.body) < maxBuf {
|
|
||||||
space := maxBuf - len(r.body)
|
|
||||||
if len(bytes) <= space {
|
|
||||||
r.body = append(r.body, bytes...)
|
|
||||||
} else {
|
|
||||||
r.body = append(r.body, bytes[:space]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PhysicalWrite 物理写入网线,绕过过滤器缓冲逻辑
|
// PhysicalWrite 物理写入网线,绕过过滤器缓冲逻辑
|
||||||
func (r *Response) PhysicalWrite(bytes []byte) (int, error) {
|
func (r *Response) PhysicalWrite(bytes []byte) (int, error) {
|
||||||
r.checkWriteHeader()
|
r.checkWriteHeader()
|
||||||
|
|||||||
15
server.go
15
server.go
@ -110,17 +110,6 @@ var DefaultServer = NewWebServer()
|
|||||||
// Config 全局配置对象 (指向 DefaultServer.Config)
|
// Config 全局配置对象 (指向 DefaultServer.Config)
|
||||||
var Config = &DefaultServer.Config
|
var Config = &DefaultServer.Config
|
||||||
|
|
||||||
func init() {
|
|
||||||
// NoLogRequestHeaders / NoLogResponseHeaders 的默认值在日志捕获时动态构建
|
|
||||||
Config.LogInputObjectNum = 10
|
|
||||||
Config.LogInputArrayNum = 5
|
|
||||||
Config.LogInputFieldSize = 20
|
|
||||||
Config.LogOutputObjectNum = 5
|
|
||||||
Config.LogOutputArrayNum = 3
|
|
||||||
Config.LogOutputFieldSize = 20
|
|
||||||
Config.LogOutputMaxSize = 200
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWebServer() *WebServer {
|
func NewWebServer() *WebServer {
|
||||||
ws := &WebServer{
|
ws := &WebServer{
|
||||||
webServices: make(map[string]map[string]*webServiceType),
|
webServices: make(map[string]map[string]*webServiceType),
|
||||||
@ -145,10 +134,6 @@ func NewWebServer() *WebServer {
|
|||||||
webAuthCheckers: make(map[int]func(int, *log.Logger, *string, map[string]any, *Request, *Response, *WebServiceOptions) (pass bool, object any)),
|
webAuthCheckers: make(map[int]func(int, *log.Logger, *string, map[string]any, *Request, *Response, *WebServiceOptions) (pass bool, object any)),
|
||||||
injectObjects: make(map[reflect.Type]any),
|
injectObjects: make(map[reflect.Type]any),
|
||||||
injectFunctions: make(map[reflect.Type]func() any),
|
injectFunctions: make(map[reflect.Type]func() any),
|
||||||
|
|
||||||
usedSessionIdKey: "Session-ID",
|
|
||||||
usedDeviceIdKey: "Device-ID",
|
|
||||||
usedClientAppKey: "App",
|
|
||||||
}
|
}
|
||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
|||||||
47
session.go
47
session.go
@ -1,15 +1,14 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/jsmod"
|
"apigo.cc/go/jsmod"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
"apigo.cc/go/redis"
|
"apigo.cc/go/redis"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session 会话对象
|
// Session 会话对象
|
||||||
@ -75,36 +74,12 @@ func (s *Session) Get(key string) any {
|
|||||||
return s.data[key]
|
return s.data[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 批量读取会话数据,keys 为空时返回全部数据
|
// Remove 移除会话数据
|
||||||
func (s *Session) Load(keys []string) map[string]any {
|
func (s *Session) Remove(key string) {
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
if len(keys) == 0 {
|
|
||||||
result := make(map[string]any, len(s.data))
|
|
||||||
for k, v := range s.data {
|
|
||||||
result[k] = v
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[string]any, len(keys))
|
|
||||||
for _, key := range keys {
|
|
||||||
if v, ok := s.data[key]; ok {
|
|
||||||
result[key] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 移除会话数据,支持传入多个 key
|
|
||||||
func (s *Session) Remove(keys ...string) {
|
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
for _, key := range keys {
|
|
||||||
delete(s.data, key)
|
delete(s.data, key)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuthLevel 设置鉴权级别
|
// SetAuthLevel 设置鉴权级别
|
||||||
func (s *Session) SetAuthLevel(level int) {
|
func (s *Session) SetAuthLevel(level int) {
|
||||||
@ -116,17 +91,11 @@ func (s *Session) GetAuthLevel() int {
|
|||||||
return cast.Int(s.Get("_authLevel"))
|
return cast.Int(s.Get("_authLevel"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save 保存会话数据,可选传入 map 用于批量设置后保存
|
// Save 保存会话数据
|
||||||
func (s *Session) Save(args ...map[string]any) error {
|
func (s *Session) Save() error {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
if len(args) > 0 && args[0] != nil {
|
|
||||||
for k, v := range args[0] {
|
|
||||||
s.data[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout := Config.SessionTimeout
|
timeout := Config.SessionTimeout
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 3600
|
timeout = 3600
|
||||||
|
|||||||
@ -27,89 +27,6 @@ func TestSessionLogic(t *testing.T) {
|
|||||||
t.Errorf("Expected value1 in new session instance, got %v", sess2.Get("key1"))
|
t.Errorf("Expected value1 in new session instance, got %v", sess2.Get("key1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.1 测试 Save 批量设置
|
|
||||||
sess3 := NewSession("test_batch", nil)
|
|
||||||
m := map[string]any{"a": 1, "b": "hello", "c": true}
|
|
||||||
if err := sess3.Save(m); err != nil {
|
|
||||||
t.Errorf("Save with map failed: %v", err)
|
|
||||||
}
|
|
||||||
sess4 := NewSession("test_batch", nil)
|
|
||||||
if sess4.Get("a") != 1 {
|
|
||||||
t.Errorf("Expected a=1, got %v", sess4.Get("a"))
|
|
||||||
}
|
|
||||||
if sess4.Get("b") != "hello" {
|
|
||||||
t.Errorf("Expected b=hello, got %v", sess4.Get("b"))
|
|
||||||
}
|
|
||||||
if sess4.Get("c") != true {
|
|
||||||
t.Errorf("Expected c=true, got %v", sess4.Get("c"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.2 测试 Save 无参数仍然正常工作
|
|
||||||
sess5 := NewSession("test_noarg", nil)
|
|
||||||
sess5.Set("x", "y")
|
|
||||||
if err := sess5.Save(); err != nil {
|
|
||||||
t.Errorf("Save without args failed: %v", err)
|
|
||||||
}
|
|
||||||
sess6 := NewSession("test_noarg", nil)
|
|
||||||
if sess6.Get("x") != "y" {
|
|
||||||
t.Errorf("Expected x=y, got %v", sess6.Get("x"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.3 测试 Load 批量读取
|
|
||||||
sess7 := NewSession("test_load", nil)
|
|
||||||
sess7.Set("k1", "v1")
|
|
||||||
sess7.Set("k2", "v2")
|
|
||||||
sess7.Set("k3", "v3")
|
|
||||||
_ = sess7.Save()
|
|
||||||
|
|
||||||
sess8 := NewSession("test_load", nil)
|
|
||||||
result := sess8.Load([]string{"k1", "k3"})
|
|
||||||
if len(result) != 2 {
|
|
||||||
t.Errorf("Expected 2 keys, got %d", len(result))
|
|
||||||
}
|
|
||||||
if result["k1"] != "v1" {
|
|
||||||
t.Errorf("Expected k1=v1, got %v", result["k1"])
|
|
||||||
}
|
|
||||||
if result["k3"] != "v3" {
|
|
||||||
t.Errorf("Expected k3=v3, got %v", result["k3"])
|
|
||||||
}
|
|
||||||
if _, ok := result["k2"]; ok {
|
|
||||||
t.Error("k2 should not be in partial Load result")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.4 测试 Load 空参数返回全部数据
|
|
||||||
allData := sess8.Load(nil)
|
|
||||||
if len(allData) < 3 {
|
|
||||||
t.Errorf("Expected at least 3 keys in full Load, got %d", len(allData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.5 测试 Remove 多 key
|
|
||||||
sess9 := NewSession("test_remove", nil)
|
|
||||||
sess9.Set("a", 1)
|
|
||||||
sess9.Set("b", 2)
|
|
||||||
sess9.Set("c", 3)
|
|
||||||
sess9.Remove("a", "c")
|
|
||||||
_ = sess9.Save()
|
|
||||||
|
|
||||||
sess10 := NewSession("test_remove", nil)
|
|
||||||
if sess10.Get("a") != nil {
|
|
||||||
t.Errorf("Expected a removed, got %v", sess10.Get("a"))
|
|
||||||
}
|
|
||||||
if sess10.Get("b") != 2 {
|
|
||||||
t.Errorf("Expected b=2, got %v", sess10.Get("b"))
|
|
||||||
}
|
|
||||||
if sess10.Get("c") != nil {
|
|
||||||
t.Errorf("Expected c removed, got %v", sess10.Get("c"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.6 测试 Remove 无参数(安全无操作)
|
|
||||||
sess9.Remove()
|
|
||||||
_ = sess9.Save()
|
|
||||||
sess11 := NewSession("test_remove", nil)
|
|
||||||
if sess11.Get("b") != 2 {
|
|
||||||
t.Errorf("Expected b=2 after no-arg Remove, got %v", sess11.Get("b"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 测试 AuthFuncs 逻辑
|
// 2. 测试 AuthFuncs 逻辑
|
||||||
sess.Set("funcs", []string{"user.read", "user.write", "system.admin"})
|
sess.Set("funcs", []string{"user.read", "user.write", "system.admin"})
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user