Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8369d4680 | ||
|
|
8f85c503da | ||
|
|
c4b8c2d3eb | ||
|
|
e75cec8aaf | ||
|
|
556d60661c | ||
|
|
eeb1032c12 |
883
.log.meta.json
883
.log.meta.json
@ -1,883 +0,0 @@
|
||||
{
|
||||
"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,5 +1,55 @@
|
||||
# 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)
|
||||
- **错误堆栈重构**:
|
||||
- 重构 `js_export.go`,将匿名占位工厂函数改写为包级具名函数。
|
||||
|
||||
14
TEST.md
14
TEST.md
@ -1,9 +1,9 @@
|
||||
# Service Module Test Report
|
||||
|
||||
## 性能测试 (Benchmark)
|
||||
- 测试日期: 2026-06-21
|
||||
- 版本: v1.0.4
|
||||
- 指标: `BenchmarkRouting`: 2791 ns/op
|
||||
- 测试日期: 2026-06-22
|
||||
- 版本: v1.5.20
|
||||
- 指标: `BenchmarkRouting`: **5394 ns/op**
|
||||
- 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||
|
||||
## 单元测试覆盖 (Unit Test)
|
||||
@ -24,7 +24,13 @@
|
||||
- [x] `TestGetDefaultName`: 自动应用名识别
|
||||
- [x] `TestGetServerIp`: 自动 IP 探测
|
||||
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
||||
- [x] **Logging Filters**: 已手动验证 `NoLogGets`, `NoLogHeaders` 等过滤逻辑。
|
||||
- [x] `TestSanitizeScalars` ~ `TestSanitizeMixedSlice`: 日志脱敏 10 个测试(标量/对象/数组/嵌套/预算/Unicode)
|
||||
- [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` 用于参数解析与类型强转。
|
||||
|
||||
19
config.go
19
config.go
@ -23,12 +23,19 @@ type ServiceConfig struct {
|
||||
Listen string // 监听端口(|隔开多个监听)(,隔开多个选项),例如 80,http|443|443:h2|127.0.0.1:8080,h2c
|
||||
SSL map[string]*CertSet // SSL 证书配置,key 为域名
|
||||
NoLogGets bool // 不记录 GET 请求的日志
|
||||
NoLogHeaders string // 不记录请求头中包含的这些字段,多个字段用逗号分隔
|
||||
LogInputArrayNum int // 请求字段中容器类型在日志打印个数限制
|
||||
LogInputFieldSize int // 请求字段中单个字段在日志打印长度限制
|
||||
NoLogOutputFields string // 不记录响应字段中包含的这些字段
|
||||
LogOutputArrayNum int // 响应字段中容器类型在日志打印个数限制
|
||||
LogOutputFieldSize int // 响应字段中单个字段在日志打印长度限制
|
||||
NoLogInput bool // 不记录请求输入
|
||||
NoLogOutput bool // 不记录响应输出
|
||||
NoLogAllHeaders bool // 不记录所有请求/响应头
|
||||
NoLogRequestHeaders string // 不记录请求头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
|
||||
NoLogResponseHeaders string // 不记录响应头中包含的这些字段(追加到动态默认列表),多个字段用逗号分隔
|
||||
LogInputObjectNum int // 请求对象中最多记录的 key 数
|
||||
LogInputArrayNum int // 请求数组中最多记录的元素数
|
||||
LogInputFieldSize int // 请求单个字段的字符串截断长度
|
||||
NoLogResponseFields string // 不记录响应字段中包含的这些字段
|
||||
LogOutputObjectNum int // 响应对象中最多记录的 key 数
|
||||
LogOutputArrayNum int // 响应数组中最多记录的元素数
|
||||
LogOutputFieldSize int // 响应单个字段的字符串截断长度
|
||||
LogOutputMaxSize int // 非对象响应内容的最大记录长度
|
||||
Compress bool // 是否启用压缩
|
||||
CompressMinSize int // 启用压缩的最小长度
|
||||
CompressMaxSize int // 启用压缩的最大长度
|
||||
|
||||
6
go.mod
6
go.mod
@ -8,10 +8,10 @@ require (
|
||||
apigo.cc/go/discover v1.5.3
|
||||
apigo.cc/go/file v1.5.5
|
||||
apigo.cc/go/http v1.5.3
|
||||
apigo.cc/go/id v1.5.4
|
||||
apigo.cc/go/id v1.5.6
|
||||
apigo.cc/go/jsmod v1.5.3
|
||||
apigo.cc/go/log v1.5.8
|
||||
apigo.cc/go/redis v1.5.6
|
||||
apigo.cc/go/redis v1.5.10
|
||||
apigo.cc/go/safe v1.5.2
|
||||
apigo.cc/go/starter v1.5.5
|
||||
apigo.cc/go/timer v1.5.0
|
||||
@ -24,7 +24,7 @@ require (
|
||||
apigo.cc/go/crypto v1.5.3 // indirect
|
||||
apigo.cc/go/encoding v1.5.4 // indirect
|
||||
apigo.cc/go/rand v1.5.3 // indirect
|
||||
apigo.cc/go/shell v1.5.3 // indirect
|
||||
apigo.cc/go/shell v1.5.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // 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/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ=
|
||||
apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
|
||||
apigo.cc/go/id v1.5.4 h1:D1Zx9gEZhOgdTgZ4SdmPImhpc9xGiOA33Y+j2MkstzQ=
|
||||
apigo.cc/go/id v1.5.4/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
|
||||
apigo.cc/go/id v1.5.5 h1:fQXfb2WZ4hEtzXkpb9w9o8AOcTZ44fYQfTV6iZ49l8o=
|
||||
apigo.cc/go/id v1.5.5/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
|
||||
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/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0=
|
||||
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/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
||||
apigo.cc/go/redis v1.5.6 h1:Lzo8M2binfqdQdVVp31Z/Max4qT8D82QdZjLlLQsrIY=
|
||||
apigo.cc/go/redis v1.5.6/go.mod h1:HmqSh2Ll7/b2zFXDi2Ap13YOuMCVniuZNbwtxkbIYII=
|
||||
apigo.cc/go/redis v1.5.9 h1:h77XPjQWcIDgxrtwjtS/gwT6PS5nSk6/x785t0OMlbY=
|
||||
apigo.cc/go/redis v1.5.9/go.mod h1:YZRDLA3gWw9LAc4Z/4GrDEy8eMDMdDPDA0iqSEAu1fE=
|
||||
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/shell v1.5.3 h1:pI+u12sy6upoygq+1XXqUlvUboBfH4Q52jRpoJFv56A=
|
||||
apigo.cc/go/shell v1.5.3/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
||||
apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc=
|
||||
apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
||||
apigo.cc/go/starter v1.5.5 h1:4ST02o4qP8IIekxtd9Jhx5RHTrSGXtVQUguSIXV0iWc=
|
||||
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=
|
||||
|
||||
117
handler.go
117
handler.go
@ -1,10 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/discover"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/timer"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -13,6 +9,11 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/discover"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/timer"
|
||||
)
|
||||
|
||||
type RouteHandler struct {
|
||||
@ -71,43 +72,33 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
usedTime := float32(tracker.Stop().Seconds())
|
||||
|
||||
// 过滤请求头
|
||||
reqHeaders := make(map[string]string)
|
||||
noLogHeaders := strings.Split(ws.Config.NoLogHeaders, ",")
|
||||
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 reqHeaders map[string]string
|
||||
if !ws.Config.NoLogAllHeaders {
|
||||
reqHeaders = sanitizeLogHeaders(r.Header, ws.effectiveNoLogRequestHeaders())
|
||||
}
|
||||
|
||||
// 过滤响应头
|
||||
respHeaders := make(map[string]string)
|
||||
for k, v := range response.Header().H {
|
||||
respHeaders[k] = strings.Join(v, ", ")
|
||||
// 响应头
|
||||
var respHeaders map[string]string
|
||||
if !ws.Config.NoLogAllHeaders {
|
||||
respHeaders = sanitizeLogHeaders(response.Header().H, ws.effectiveNoLogResponseHeaders())
|
||||
}
|
||||
|
||||
// 处理响应内容截断
|
||||
// 请求输入脱敏
|
||||
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
|
||||
if response.Code != 200 {
|
||||
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]"
|
||||
}
|
||||
if !ws.Config.NoLogOutput && len(response.body) > 0 {
|
||||
respData = sanitizeRespBody(response.body, &ws.Config)
|
||||
}
|
||||
|
||||
LogRequest(requestLogger, func(entry *RequestLog) {
|
||||
@ -128,7 +119,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
entry.AuthLevel = authLevel
|
||||
entry.Priority = priority
|
||||
entry.RequestHeaders = reqHeaders
|
||||
entry.RequestData = args
|
||||
entry.RequestData = reqData
|
||||
entry.ResponseCode = response.Code
|
||||
entry.UsedTime = usedTime
|
||||
entry.ResponseHeaders = respHeaders
|
||||
@ -433,6 +424,7 @@ func outputResult(response *Response, result any) {
|
||||
response.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
response.keepBody(data)
|
||||
if response.server != nil && response.server.hasOutFilter {
|
||||
response.PhysicalWrite(data)
|
||||
} else {
|
||||
@ -442,8 +434,11 @@ func outputResult(response *Response, result any) {
|
||||
func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
|
||||
// SessionId
|
||||
if ws.usedSessionIdKey != "" {
|
||||
hasRequestSessionId := false
|
||||
sessionId := request.Header().Get(ws.usedSessionIdKey)
|
||||
if sessionId == "" && !ws.Config.SessionWithoutCookie {
|
||||
if sessionId != "" {
|
||||
hasRequestSessionId = true
|
||||
} else if !ws.Config.SessionWithoutCookie {
|
||||
if ck := request.GetCookie(ws.usedSessionIdKey); ck != nil {
|
||||
sessionId = ck.Value
|
||||
}
|
||||
@ -464,13 +459,18 @@ func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
|
||||
}
|
||||
}
|
||||
request.Request.Header.Set(discover.HeaderSessionID, sessionId)
|
||||
response.Header().Set(ws.usedSessionIdKey, sessionId)
|
||||
if !hasRequestSessionId {
|
||||
response.Header().Set(ws.usedSessionIdKey, sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
// DeviceId
|
||||
if ws.usedDeviceIdKey != "" {
|
||||
hasRequestDeviceId := false
|
||||
deviceId := request.Header().Get(ws.usedDeviceIdKey)
|
||||
if deviceId == "" && !ws.Config.DeviceWithoutCookie {
|
||||
if deviceId != "" {
|
||||
hasRequestDeviceId = true
|
||||
} else if !ws.Config.DeviceWithoutCookie {
|
||||
if ck := request.GetCookie(ws.usedDeviceIdKey); ck != nil {
|
||||
deviceId = ck.Value
|
||||
}
|
||||
@ -488,6 +488,41 @@ func (ws *WebServer) handleClientKeys(request *Request, response *Response) {
|
||||
}
|
||||
}
|
||||
request.Request.Header.Set(discover.HeaderDeviceID, deviceId)
|
||||
response.Header().Set(ws.usedDeviceIdKey, deviceId)
|
||||
if !hasRequestDeviceId {
|
||||
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
|
||||
}
|
||||
|
||||
// jsUploadFile 包装 UploadFile 以隐藏敏感方法
|
||||
// jsUploadFile 包装 UploadFile 以隐藏敏感方法(如 Open)
|
||||
type jsUploadFile struct {
|
||||
f *UploadFile
|
||||
}
|
||||
|
||||
6
log.go
6
log.go
@ -25,12 +25,12 @@ type RequestLog struct {
|
||||
AuthLevel int `log:"pos:22,color:green"`
|
||||
Priority int `log:"pos:23,hide:true"`
|
||||
RequestData any `log:"pos:24,color:cyan,keyname:Request"`
|
||||
RequestHeaders map[string]string `log:"pos:25,color:cyan,keyname:Headers"`
|
||||
RequestHeaders map[string]string `log:"pos:25,color:blue,keyname:Headers"`
|
||||
UsedTime float32 `log:"pos:26,color:green,precision:6"`
|
||||
ResponseCode int `log:"pos:27,color:magenta,keyname:Status"`
|
||||
ResponseDataLength uint `log:"pos:28,color:magenta,keyname:ContentLength"`
|
||||
ResponseDataLength uint `log:"pos:28,color:magenta,keyname:Size"`
|
||||
ResponseData any `log:"pos:29,color:magenta,keyname:Response"`
|
||||
ResponseHeaders map[string]string `log:"pos:30,color:magenta,keyname:Headers"`
|
||||
ResponseHeaders map[string]string `log:"pos:30,color:yellow,keyname:Headers"`
|
||||
}
|
||||
|
||||
func (l *RequestLog) Reset() {
|
||||
|
||||
242
log_sanitize.go
Normal file
242
log_sanitize.go
Normal file
@ -0,0 +1,242 @@
|
||||
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
|
||||
}
|
||||
250
log_sanitize_test.go
Normal file
250
log_sanitize_test.go
Normal file
@ -0,0 +1,250 @@
|
||||
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,10 +67,8 @@ func (r *Response) Write(bytes []byte) (int, error) {
|
||||
return len(bytes), nil
|
||||
}
|
||||
|
||||
// 即使没有过滤器,非 200 状态码也进行缓冲以便日志记录
|
||||
if r.Code != http.StatusOK {
|
||||
r.body = append(r.body, bytes...)
|
||||
}
|
||||
// 缓冲 body 用于日志记录
|
||||
r.keepBody(bytes)
|
||||
|
||||
if r.ProxyHeader != nil {
|
||||
r.copyProxyHeader()
|
||||
@ -82,6 +80,22 @@ func (r *Response) Write(bytes []byte) (int, error) {
|
||||
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 物理写入网线,绕过过滤器缓冲逻辑
|
||||
func (r *Response) PhysicalWrite(bytes []byte) (int, error) {
|
||||
r.checkWriteHeader()
|
||||
|
||||
15
server.go
15
server.go
@ -110,6 +110,17 @@ var DefaultServer = NewWebServer()
|
||||
// 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 {
|
||||
ws := &WebServer{
|
||||
webServices: make(map[string]map[string]*webServiceType),
|
||||
@ -134,6 +145,10 @@ func NewWebServer() *WebServer {
|
||||
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),
|
||||
injectFunctions: make(map[reflect.Type]func() any),
|
||||
|
||||
usedSessionIdKey: "Session-ID",
|
||||
usedDeviceIdKey: "Device-ID",
|
||||
usedClientAppKey: "App",
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
49
session.go
49
session.go
@ -1,14 +1,15 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/jsmod"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/redis"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/jsmod"
|
||||
"apigo.cc/go/log"
|
||||
"apigo.cc/go/redis"
|
||||
)
|
||||
|
||||
// Session 会话对象
|
||||
@ -74,11 +75,35 @@ func (s *Session) Get(key string) any {
|
||||
return s.data[key]
|
||||
}
|
||||
|
||||
// Remove 移除会话数据
|
||||
func (s *Session) Remove(key string) {
|
||||
// Load 批量读取会话数据,keys 为空时返回全部数据
|
||||
func (s *Session) Load(keys []string) map[string]any {
|
||||
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()
|
||||
defer s.lock.Unlock()
|
||||
delete(s.data, key)
|
||||
for _, key := range keys {
|
||||
delete(s.data, key)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthLevel 设置鉴权级别
|
||||
@ -91,11 +116,17 @@ func (s *Session) GetAuthLevel() int {
|
||||
return cast.Int(s.Get("_authLevel"))
|
||||
}
|
||||
|
||||
// Save 保存会话数据
|
||||
func (s *Session) Save() error {
|
||||
// Save 保存会话数据,可选传入 map 用于批量设置后保存
|
||||
func (s *Session) Save(args ...map[string]any) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if len(args) > 0 && args[0] != nil {
|
||||
for k, v := range args[0] {
|
||||
s.data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
timeout := Config.SessionTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 3600
|
||||
|
||||
@ -27,6 +27,89 @@ func TestSessionLogic(t *testing.T) {
|
||||
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 逻辑
|
||||
sess.Set("funcs", []string{"user.read", "user.write", "system.admin"})
|
||||
|
||||
|
||||
@ -131,6 +131,10 @@ func (ws *WebServer) processStatic(requestPath string, request *Request, respons
|
||||
return false
|
||||
}
|
||||
|
||||
// 静态文件通过 Cookie 维护 ID,不应答 Device-Id / Session-Id 头
|
||||
response.Header().Del(ws.usedDeviceIdKey)
|
||||
response.Header().Del(ws.usedSessionIdKey)
|
||||
|
||||
// 检查 304
|
||||
if ifModifiedSince := request.Header().Get("If-Modified-Since"); ifModifiedSince != "" {
|
||||
if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil {
|
||||
|
||||
@ -78,6 +78,11 @@ func Upgrade(response *Response, request *Request) (*WebSocketConn, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Error("websocket upgrade failed", "error", err.Error())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user