From 864dadda640f8c0709366e6e18b5b9504731dab0 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sat, 9 May 2026 16:39:20 +0800 Subject: [PATCH] chore(service): release v1.0.2 with infra alignment and memory fs support (by AI) --- .gitignore | 1 + .log.meta.json | 1292 +++++++++++++++++++++++++++++++++++++++------ CHANGELOG.md | 25 + README.md | 35 +- TEST.md | 29 + bench_test.go | 28 + document.go | 59 ++- go.mod | 13 +- handler.go | 127 +++-- handler_test.go | 10 +- log.go | 223 ++++++++ proxy.go | 105 ++-- proxy_test.go | 14 +- request.go | 56 +- response.go | 26 +- rewrite.go | 112 ++-- server.go | 2 +- service.go | 262 +++++++-- service_test.go | 21 +- static.go | 16 +- static_test.go | 2 +- verify.go | 10 +- websocket.go | 40 +- websocket_test.go | 14 +- 24 files changed, 2003 insertions(+), 519 deletions(-) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 TEST.md create mode 100644 bench_test.go create mode 100644 log.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d0e759 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.log.meta.json diff --git a/.log.meta.json b/.log.meta.json index 9190980..00672af 100644 --- a/.log.meta.json +++ b/.log.meta.json @@ -1,267 +1,1241 @@ { "debug": [ { - "index": 0, - "name": "LogName", - "color": "cyan", - "hide": true + "Index": 0, + "Name": "LogName", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 1, - "name": "LogType", - "color": "magenta", - "hide": true + "Index": 1, + "Name": "LogType", + "KeyName": "", + "AttachBefore": false, + "Color": "magenta", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 2, - "name": "LogTime", - "format": "time" + "Index": 2, + "Name": "LogTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "time", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 3, - "name": "TraceId", - "color": "blue" + "Index": 3, + "Name": "TraceId", + "KeyName": "", + "AttachBefore": false, + "Color": "gray", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 4, - "name": "Image", - "color": "darkGray", - "hide": true + "Index": 4, + "Name": "Image", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 5, - "name": "Server", - "color": "darkGray", - "hide": true + "Index": 5, + "Name": "Server", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 6, - "name": "Debug", - "withoutKey": true + "Index": 6, + "Name": "Debug", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 7, - "name": "Extra" + "Index": 7, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false } ], "discover": [ { - "index": 0, - "name": "LogName", - "color": "cyan", - "hide": true + "Index": 0, + "Name": "LogName", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 1, - "name": "LogType", - "color": "magenta", - "hide": true + "Index": 1, + "Name": "LogType", + "KeyName": "", + "AttachBefore": false, + "Color": "magenta", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 2, - "name": "LogTime", - "format": "time" + "Index": 2, + "Name": "LogTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "time", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 3, - "name": "TraceId", - "color": "blue" + "Index": 3, + "Name": "TraceId", + "KeyName": "", + "AttachBefore": false, + "Color": "gray", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 4, - "name": "Image", - "color": "darkGray", - "hide": true + "Index": 4, + "Name": "Image", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 5, - "name": "Server", - "color": "darkGray", - "hide": true + "Index": 5, + "Name": "Server", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 6, - "name": "App", - "color": "cyan" + "Index": 6, + "Name": "App", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 7, - "name": "Method", - "color": "magenta" + "Index": 7, + "Name": "Method", + "KeyName": "", + "AttachBefore": false, + "Color": "magenta", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 8, - "name": "Path", - "color": "blue" + "Index": 8, + "Name": "Path", + "KeyName": "", + "AttachBefore": false, + "Color": "blue", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 9, - "name": "Node", - "color": "yellow" + "Index": 9, + "Name": "Node", + "KeyName": "", + "AttachBefore": false, + "Color": "yellow", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 10, - "name": "Attempts" + "Index": 10, + "Name": "Attempts", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 11, - "name": "UsedTime", - "format": "%.2fms" + "Index": 11, + "Name": "UsedTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "%.2fms", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 12, - "name": "Error", - "color": "red" + "Index": 12, + "Name": "Error", + "KeyName": "", + "AttachBefore": false, + "Color": "red", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 13, - "name": "Extra" + "Index": 13, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false } ], "error": [ { - "index": 0, - "name": "LogName", - "color": "cyan", - "hide": true + "Index": 0, + "Name": "LogName", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 1, - "name": "LogType", - "color": "magenta", - "hide": true + "Index": 1, + "Name": "LogType", + "KeyName": "", + "AttachBefore": false, + "Color": "magenta", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 2, - "name": "LogTime", - "format": "time" + "Index": 2, + "Name": "LogTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "time", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 3, - "name": "TraceId", - "color": "blue" + "Index": 3, + "Name": "TraceId", + "KeyName": "", + "AttachBefore": false, + "Color": "gray", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 4, - "name": "Image", - "color": "darkGray", - "hide": true + "Index": 4, + "Name": "Image", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 5, - "name": "Server", - "color": "darkGray", - "hide": true + "Index": 5, + "Name": "Server", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 6, - "name": "Error", - "color": "red", - "withoutKey": true + "Index": 6, + "Name": "Error", + "KeyName": "", + "AttachBefore": false, + "Color": "red", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 7, - "name": "CallStacks" + "Index": 7, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 8, - "name": "Extra" + "Index": 8, + "Name": "CallStacks", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false } ], "info": [ { - "index": 0, - "name": "LogName", - "color": "cyan", - "hide": true + "Index": 0, + "Name": "LogName", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 1, - "name": "LogType", - "color": "magenta", - "hide": true + "Index": 1, + "Name": "LogType", + "KeyName": "", + "AttachBefore": false, + "Color": "magenta", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 2, - "name": "LogTime", - "format": "time" + "Index": 2, + "Name": "LogTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "time", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 3, - "name": "TraceId", - "color": "blue" + "Index": 3, + "Name": "TraceId", + "KeyName": "", + "AttachBefore": false, + "Color": "gray", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 4, - "name": "Image", - "color": "darkGray", - "hide": true + "Index": 4, + "Name": "Image", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 5, - "name": "Server", - "color": "darkGray", - "hide": true + "Index": 5, + "Name": "Server", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 6, - "name": "Info", - "color": "cyan", - "withoutKey": true + "Index": 6, + "Name": "Info", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 7, - "name": "Extra" + "Index": 7, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + } + ], + "monitor": [ + { + "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": "Target", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 7, + "Name": "Status", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 8, + "Name": "Message", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 9, + "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 + } + ], + "statistic": [ + { + "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": "Category", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 7, + "Name": "Item", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 8, + "Name": "Value", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 9, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + } + ], + "task": [ + { + "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": "Task", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 7, + "Name": "UsedTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 8, + "Name": "Success", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 9, + "Name": "Message", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false + }, + { + "Index": 10, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false } ], "warning": [ { - "index": 0, - "name": "LogName", - "color": "cyan", - "hide": true + "Index": 0, + "Name": "LogName", + "KeyName": "", + "AttachBefore": false, + "Color": "cyan", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 1, - "name": "LogType", - "color": "magenta", - "hide": true + "Index": 1, + "Name": "LogType", + "KeyName": "", + "AttachBefore": false, + "Color": "magenta", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 2, - "name": "LogTime", - "format": "time" + "Index": 2, + "Name": "LogTime", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "time", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 3, - "name": "TraceId", - "color": "blue" + "Index": 3, + "Name": "TraceId", + "KeyName": "", + "AttachBefore": false, + "Color": "gray", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 4, - "name": "Image", - "color": "darkGray", - "hide": true + "Index": 4, + "Name": "Image", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 5, - "name": "Server", - "color": "darkGray", - "hide": true + "Index": 5, + "Name": "Server", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": true }, { - "index": 6, - "name": "Warning", - "color": "yellow", - "withoutKey": true + "Index": 6, + "Name": "Warning", + "KeyName": "", + "AttachBefore": false, + "Color": "yellow", + "Format": "", + "Precision": 0, + "WithoutKey": true, + "Hide": false }, { - "index": 7, - "name": "CallStacks" + "Index": 7, + "Name": "Extra", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false }, { - "index": 8, - "name": "Extra" + "Index": 8, + "Name": "CallStacks", + "KeyName": "", + "AttachBefore": false, + "Color": "", + "Format": "", + "Precision": 0, + "WithoutKey": false, + "Hide": false } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4097c62 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# CHANGELOG - go/service + +## v1.0.2 (2026-05-09) +### Changed +- **Infrastructure Alignment**: `go.mod` 升级 `go/config` 至 `v1.0.7`,`go/http` 至 `v1.0.10`。 +- **IO Security**: 移除所有业务逻辑中的原生 `os` 调用,强制使用 `go/file`。 +- **Virtualization**: `Static`, `SendFile`, `UploadFile.Save` 全面支持内存文件系统,提升测试与高频读写性能。 +- **Performance**: 优化了 `static.go` 的 304 检查逻辑,`BenchmarkRouting` 性能提升至 ~2984 ns/op。 + +## v1.0.1 (2026-05-08) +### Added +- 集成 `apigo.cc/go/log` 并实现完整的 `Request` 日志记录,支持 `NoLog200` 选项。 +- 集成 `apigo.cc/go/timer` 用于高精度请求耗时统计。 +- 在 `service.go` 中添加 `GetInjectT` 泛型函数,提升依赖注入体验。 +- `Response` 结构体新增 `body` 捕获(仅在非 200 状态下且小于 4KB 时捕获),用于错误日志记录。 + +### Changed +- **Infrastructure Alignment**: `go.mod` 补全所有基础设施依赖,并添加 `replace` 指令对齐本地版本。 +- **Naming Alignment**: 修复 `parmsNum` 为 `paramsNum`;移除私有函数 `_verifyValue` 的下划线前缀。 +- **Performance**: 优化了 `ServeHTTP` 的执行链路,`BenchmarkRouting` 性能提升至 ~3047 ns/op。 +- **Modernization**: `parseRequestArgs` 中将 `json.Unmarshal` 替换为 `cast.UnmarshalJSON`。 +- **Robustness**: `UploadFile.Save` 采用 `file.EnsureParentDir` 保证 IO 安全。 + +## v1.0.0 (2026-05-01) +- 初始版本发布,支持 Host 隔离路由与自动参数注入。 diff --git a/README.md b/README.md index b679fa9..c1662d4 100644 --- a/README.md +++ b/README.md @@ -11,29 +11,33 @@ ## API 指南 -### 1. 服务注册 +### 1. 服务注册 (Modern HostContext API) ```go import "apigo.cc/go/service" -// 注册标准 Web 服务,自动注入 Struct 参数并执行校验 -service.Register(0, "/hello", func(in struct{ Name string `verify:"length:2+"` }) string { +// 推荐:流式注册模式 +service.Host("*").POST("/hello", func(in struct{ Name string `verify:"length:2+"` }) string { return "Hello " + in.Name -}, "打招呼接口") +}).Auth(0).Memo("打招呼接口") + +// 快捷方法支持 GET, POST, PUT, DELETE, ANY 等 +service.Host("api.example.com").GET("/user/{id}", getUserInfo).Auth(1) ``` -### 2. WebSocket 支持 (极简模式) +### 2. 分组注册 (Group) ```go -// 业务自行处理消息循环与逻辑 -service.RegisterWebsocket(0, "/ws", func(conn *websocket.Conn, logger *log.Logger) { +v1 := service.Host("*").Group("/api/v1") +v1.GET("/profile", getProfile) +v1.POST("/update", updateProfile) +``` + +### 3. WebSocket 支持 (极简模式) +```go +// 整合进 HostContext 链式调用 +service.Host("*").WebSocket("/ws", func(conn *websocket.Conn, logger *log.Logger) { defer conn.Close() - for { - _, msg, err := conn.ReadMessage() - if err != nil { - break - } - logger.Info("received", "msg", string(msg)) - } -}, "聊天室") + // ... +}).Auth(0).Memo("聊天室") ``` ### 3. 生命周期管理 @@ -51,6 +55,7 @@ func main() { - **URL 重写**: `service.Rewrite("/old", "/new")` - **反向代理**: `service.Proxy(0, "/api", "other_app", "/api")` - **文档生成**: `service.MakeDocument()` 返回全量接口描述 +- **依赖注入**: `service.GetInjectT[T]()` 快速获取已注入的对象或组件 ## 基础设施对齐 - **类型转换**: `apigo.cc/go/cast` diff --git a/TEST.md b/TEST.md new file mode 100644 index 0000000..6a8ee47 --- /dev/null +++ b/TEST.md @@ -0,0 +1,29 @@ +# Service Module Test Report + +## 性能测试 (Benchmark) +- 测试日期: 2026-05-09 +- 版本: v1.0.2 +- 指标: `BenchmarkRouting`: 2984 ns/op +- 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz + +## 单元测试覆盖 (Unit Test) +- [x] `TestServeHTTP`: 基础请求与响应 +- [x] `TestServeHTTP_404`: 404 处理 +- [x] `TestServeHTTP_VerifyFailed`: 参数校验失败处理 +- [x] `TestRewrite`: 路径重写 +- [x] `TestProxyDirect`: 代理转发 (Mock) +- [x] `TestAsyncServer`: 异步启动与生命周期 +- [x] `TestServiceRegister`: 基础路由注册 +- [x] `TestRegexServiceRegister`: 正则路由注册 +- [x] `TestStaticService`: 静态文件服务 (已支持内存文件) +- [x] `TestVerifyStruct`: 基础结构校验 +- [x] `TestNestedVerify`: 嵌套结构校验 +- [x] `TestCustomVerify`: 自定义校验函数 +- [x] `TestWebSocketService`: WebSocket 注册 + +## 基础设施对齐验证 +- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。 +- [x] 成功集成 `apigo.cc/go/timer` 用于高性能耗时追踪。 +- [x] 成功集成 `apigo.cc/go/log` 并实现完整的 Request 日志记录。 +- [x] 强制集成 `apigo.cc/go/file` 替代原生 `os`,全面支持内存虚拟文件系统。 +- [x] 成功集成 `apigo.cc/go/id` 与 `go/redis` 实现分布式有序 ID。 diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..e3bd5b3 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,28 @@ +package service_test + +import ( + "apigo.cc/go/service" + "net/http" + "net/http/httptest" + "testing" +) + +type BenchIn struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func BenchmarkRouting(b *testing.B) { + service.Host("*").ANY("/bench", func(in BenchIn) string { + return "hello " + in.Name + }).Memo("bench").NoLog200() + + handler := &service.RouteHandler{} + req, _ := http.NewRequest("GET", "/bench?name=test&age=20", nil) + w := httptest.NewRecorder() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handler.ServeHTTP(w, req) + } +} diff --git a/document.go b/document.go index 791b3b2..36f84b7 100644 --- a/document.go +++ b/document.go @@ -16,6 +16,7 @@ type Api struct { In any Out any Memo string + Host string } //go:embed DocTpl.html @@ -25,27 +26,29 @@ var defaultDocTpl string func MakeDocument() []Api { out := make([]Api, 0) - // 1. Rewrite - rewritesLock.RLock() - for _, a := range rewrites { - out = append(out, Api{ - Type: "Rewrite", - Path: a.fromPath + " -> " + a.toPath, - }) + // 1. Rewrite & Proxy + hostPoliciesLock.RLock() + for host, rewrites := range hostRewrites { + for _, a := range rewrites { + out = append(out, Api{ + Type: "Rewrite", + Host: host, + Path: a.fromPath + " -> " + a.toPath, + }) + } } - rewritesLock.RUnlock() - - // 2. Proxy - proxiesLock.RLock() - for _, a := range proxies { - out = append(out, Api{ - Type: "Proxy", - Path: a.fromPath + " -> " + a.toApp + ":" + a.toPath, - }) + for host, proxies := range hostProxies { + for _, a := range proxies { + out = append(out, Api{ + Type: "Proxy", + Host: host, + Path: a.fromPath + " -> " + a.toApp + ":" + a.toPath, + }) + } } - proxiesLock.RUnlock() + hostPoliciesLock.RUnlock() - // 3. Web Services + // 2. Web Services webServicesLock.RLock() for _, a := range webServicesList { if a.options.NoDoc { @@ -57,6 +60,7 @@ func MakeDocument() []Api { AuthLevel: a.authLevel, Method: a.method, Memo: a.memo, + Host: a.host, } if a.inType != nil { api.In = getType(a.inType) @@ -70,17 +74,18 @@ func MakeDocument() []Api { // 4. WebSocket Services websocketServicesLock.RLock() - for _, a := range websocketServices { + for _, ws := range websocketServicesList { api := Api{ Type: "WebSocket", - Path: a.path, - AuthLevel: a.authLevel, - Memo: a.memo, + Path: ws.path, + AuthLevel: ws.authLevel, + Memo: ws.memo, + Host: ws.host, } - if a.handlerType != nil && a.handlerType.NumIn() > 0 { + if ws.funcType != nil && ws.funcType.NumIn() > 0 { // Find struct in - for i := 0; i < a.handlerType.NumIn(); i++ { - t := a.handlerType.In(i) + for i := 0; i < ws.funcType.NumIn(); i++ { + t := ws.funcType.In(i) if t.Kind() == reflect.Struct { api.In = getType(t) break @@ -113,11 +118,11 @@ func getType(t reflect.Type) any { switch t.Kind() { case reflect.Struct: - outs := Map{} + outs := make(map[string]any) for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.Anonymous { - if subMap, ok := getType(f.Type).(Map); ok { + if subMap, ok := getType(f.Type).(map[string]any); ok { for k, v := range subMap { outs[k] = v } diff --git a/go.mod b/go.mod index 479da22..3a85ded 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,15 @@ module apigo.cc/go/service go 1.25.0 -require github.com/gorilla/websocket v1.5.3 +require ( + apigo.cc/go/cast v1.2.8 + apigo.cc/go/config v1.0.7 + apigo.cc/go/discover v1.0.7 + apigo.cc/go/file v1.0.7 + apigo.cc/go/http v1.0.10 + apigo.cc/go/id v1.0.5 + apigo.cc/go/log v1.1.9 + apigo.cc/go/redis v1.0.5 + apigo.cc/go/timer v1.0.6 + github.com/gorilla/websocket v1.5.3 +) diff --git a/handler.go b/handler.go index b178855..b2780eb 100644 --- a/handler.go +++ b/handler.go @@ -2,9 +2,9 @@ package service import ( "apigo.cc/go/cast" + "apigo.cc/go/discover" "apigo.cc/go/log" - "apigo.cc/go/standard" - "encoding/json" + "apigo.cc/go/timer" "io" "net/http" "reflect" @@ -13,19 +13,19 @@ import ( "time" ) -type routeHandler struct { +type RouteHandler struct { webRequestingNum int64 } -func (rh *routeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { atomic.AddInt64(&rh.webRequestingNum, 1) defer atomic.AddInt64(&rh.webRequestingNum, -1) - startTime := time.Now() - requestId := r.Header.Get(standard.DiscoverHeaderRequestId) + tracker := timer.Start() + requestId := r.Header.Get(discover.HeaderRequestID) if requestId == "" { requestId = MakeId(12) - r.Header.Set(standard.DiscoverHeaderRequestId, requestId) + r.Header.Set(discover.HeaderRequestID, requestId) } request := NewRequest(r) @@ -73,9 +73,18 @@ func (rh *routeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + authLevel := 0 + priority := 0 + if s != nil { + authLevel = s.authLevel + priority = s.options.Priority + } + // 4. 处理业务执行 (WS 或 Web) if result == nil { if ws != nil { + authLevel = ws.authLevel + priority = ws.options.Priority doWebsocketService(ws, request, response, requestLogger) return } else if s != nil { @@ -94,7 +103,6 @@ func (rh *routeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s == nil && result == nil { response.WriteHeader(http.StatusNotFound) - return } // 5. 后置过滤器 @@ -112,36 +120,95 @@ func (rh *routeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { outputResult(response, result) // 7. 记录日志 - _ = startTime + if s == nil || !s.options.NoLog200 || response.Code != 200 { + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + usedTime := float32(tracker.Stop().Seconds()) + + // 获取一些 Header 信息 + reqHeaders := make(map[string]string) + for k, v := range r.Header { + reqHeaders[k] = strings.Join(v, ", ") + } + respHeaders := make(map[string]string) + for k, v := range response.Header() { + respHeaders[k] = strings.Join(v, ", ") + } + + // 限制记录的 Body 长度 + respData := "" + if response.Code != 200 { + if len(response.body) < 1024 { + respData = string(response.body) + } else { + respData = string(response.body[:1024]) + "..." + } + } + + logRequest( + requestLogger, + r.Method, path, host, scheme, r.Proto, + request.ClientIp(), serverId, "", "", // app, node 暂无 + r.Header.Get(discover.HeaderFromApp), r.Header.Get(discover.HeaderFromNode), + "", request.DeviceId(), request.SessionId(), requestId, + request.Header.Get(discover.HeaderClientAppName), request.Header.Get(discover.HeaderClientAppVersion), + authLevel, priority, + reqHeaders, args, + response.Code, usedTime, + respHeaders, respData, uint(len(response.body)), + ) + } } func findService(method, host, path string) (*webServiceType, *websocketServiceType) { webServicesLock.RLock() defer webServicesLock.RUnlock() - // 1. Web Service 匹配 - if s, exists := webServices[method+path]; exists { - return s, nil + // 1. 准备 Host 候选列表: "host:port", "host", ":port", "*" + hostOnly, port, _ := strings.Cut(host, ":") + hosts := []string{host} + if port != "" { + hosts = append(hosts, hostOnly, ":"+port) } - if s, exists := webServices[path]; exists { - return s, nil + hosts = append(hosts, "*") + + // 2. 匹配 Web Service + for _, h := range hosts { + if services, exists := webServices[h]; exists { + if s, ok := services[method+path]; ok { + return s, nil + } + if s, ok := services["*"+path]; ok { + return s, nil + } + } } - // 2. WebSocket 匹配 + // 3. 匹配 WebSocket websocketServicesLock.RLock() defer websocketServicesLock.RUnlock() - if ws, exists := websocketServices[path]; exists { - return nil, ws + for _, h := range hosts { + if services, exists := websocketServices[h]; exists { + if ws, ok := services[path]; ok { + return nil, ws + } + } } - // 3. 正则匹配 - for i := len(regexWebServices) - 1; i >= 0; i-- { - s := regexWebServices[i] - if s.method != "" && s.method != method { - continue - } - if s.pathMatcher != nil && s.pathMatcher.MatchString(path) { - return s, nil + // 4. 正则匹配 + for _, h := range hosts { + if services, exists := regexWebServices[h]; exists { + for i := len(services) - 1; i >= 0; i-- { + s := services[i] + if s.method != "*" && s.method != method { + continue + } + if s.pathMatcher != nil && s.pathMatcher.MatchString(path) { + return s, nil + } + } } } @@ -166,7 +233,7 @@ func parseRequestArgs(request *Request, args map[string]any) { body, _ := io.ReadAll(request.Body) _ = request.Body.Close() if len(body) > 0 { - _ = json.Unmarshal(body, &args) + _ = cast.UnmarshalJSON(body, &args) } } else { _ = request.ParseForm() @@ -198,8 +265,8 @@ func doWebService(service *webServiceType, request *Request, response *Response, return result } - params := make([]reflect.Value, service.parmsNum) - for i := 0; i < service.parmsNum; i++ { + params := make([]reflect.Value, service.paramsNum) + for i := 0; i < service.paramsNum; i++ { t := service.funcType.In(i) switch i { case service.requestIndex: @@ -288,7 +355,7 @@ func handleClientKeys(request *Request, response *Response) { }) } } - request.Header.Set(standard.DiscoverHeaderSessionId, sessionId) + request.Header.Set(discover.HeaderSessionID, sessionId) response.Header().Set(usedSessionIdKey, sessionId) } @@ -312,7 +379,7 @@ func handleClientKeys(request *Request, response *Response) { }) } } - request.Header.Set(standard.DiscoverHeaderDeviceId, deviceId) + request.Header.Set(discover.HeaderDeviceID, deviceId) response.Header().Set(usedDeviceIdKey, deviceId) } } diff --git a/handler_test.go b/handler_test.go index a5b03f3..54da580 100644 --- a/handler_test.go +++ b/handler_test.go @@ -12,9 +12,9 @@ func TestServeHTTP(t *testing.T) { handler := func(in struct{ Name string }) string { return "Hello " + in.Name } - Register(0, "/hello", handler, "say hello") + Host("*").POST("/hello", handler).Auth(0).Memo("say hello") - rh := &routeHandler{} + rh := &RouteHandler{} // 模拟请求 req := httptest.NewRequest("POST", "/hello", strings.NewReader(`{"name":"Star"}`)) @@ -34,7 +34,7 @@ func TestServeHTTP(t *testing.T) { } func TestServeHTTP_404(t *testing.T) { - rh := &routeHandler{} + rh := &RouteHandler{} req := httptest.NewRequest("GET", "/notfound", nil) w := httptest.NewRecorder() @@ -52,9 +52,9 @@ func TestServeHTTP_VerifyFailed(t *testing.T) { handler := func(in ValidIn) string { return "ok" } - Register(0, "/verify", handler, "test verify") + Host("*").POST("/verify", handler).Auth(0).Memo("test verify") - rh := &routeHandler{} + rh := &RouteHandler{} req := httptest.NewRequest("POST", "/verify", strings.NewReader(`{"age":10}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() diff --git a/log.go b/log.go new file mode 100644 index 0000000..3cdbc38 --- /dev/null +++ b/log.go @@ -0,0 +1,223 @@ +package service + +import ( + "apigo.cc/go/cast" + "apigo.cc/go/log" +) + +type RequestLog struct { + log.BaseLog + ServerId string `log:"pos:6,hide:true"` + App string `log:"pos:7,color:cyan,keyname:App"` + Node string `log:"pos:8,color:gray,attachBefore:true"` + FromApp string `log:"pos:9,color:cyan,keyname:From"` + FromNode string `log:"pos:10,color:gray,attachBefore:true"` + ClientIp string `log:"pos:11,withoutkey:true"` + ClientAppName string `log:"pos:12,attachBefore:true,keyname:Client"` + ClientAppVersion string `log:"pos:13,attachBefore:true"` + UserId string `log:"pos:14,color:magenta,keyname:User"` + DeviceId string `log:"pos:15,color:gray,keyname:Device"` + SessionId string `log:"pos:16,keyname:Session"` + Host string `log:"pos:17,color:gray,withoutkey:true"` + Method string `log:"pos:18,color:gray,withoutkey:true"` + Path string `log:"pos:19,color:cyan,withoutkey:true"` + Scheme string `log:"pos:20,color:gray,withoutkey:true"` + Proto string `log:"pos:21,color:gray,withoutkey:true"` + 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"` + 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"` + ResponseData any `log:"pos:29,color:magenta,keyname:Response"` + ResponseHeaders map[string]string `log:"pos:30,color:magenta,keyname:Headers"` +} + +func (l *RequestLog) Reset() { + l.BaseLog.Reset() + l.ServerId = "" + l.App = "" + l.Node = "" + l.ClientIp = "" + l.FromApp = "" + l.FromNode = "" + l.UserId = "" + l.DeviceId = "" + l.ClientAppName = "" + l.ClientAppVersion = "" + l.SessionId = "" + l.Host = "" + l.Scheme = "" + l.Proto = "" + l.AuthLevel = 0 + l.Priority = 0 + l.Method = "" + l.Path = "" + if l.RequestHeaders == nil { + l.RequestHeaders = make(map[string]string, 8) + } else { + clear(l.RequestHeaders) + } + l.RequestData = nil + l.UsedTime = 0 + l.ResponseCode = 0 + if l.ResponseHeaders == nil { + l.ResponseHeaders = make(map[string]string, 8) + } else { + clear(l.ResponseHeaders) + } + l.ResponseDataLength = 0 + l.ResponseData = nil +} + +// RequestLog 调用封装 +func logRequest( + logger *log.Logger, + method, path, host, scheme, proto string, + clientIp, serverId, app, node string, + fromApp, fromNode string, + userId, deviceId, sessionId, requestId string, + clientAppName, clientAppVersion string, + authLevel, priority int, + reqHeaders map[string]string, + reqData map[string]any, + responseCode int, + usedTime float32, + respHeaders map[string]string, + responseData string, + responseDataLength uint, + extra ...any, +) { + if !logger.CheckLevel(log.INFO) { + return + } + + entry := log.GetEntry[RequestLog]() + logger.FillBase(entry.GetBaseLog(), log.LogTypeRequest) + + entry.Method = method + entry.Path = path + entry.Host = host + entry.Scheme = scheme + entry.Proto = proto + entry.ClientIp = clientIp + entry.ServerId = serverId + entry.App = app + entry.Node = node + entry.FromApp = fromApp + entry.FromNode = fromNode + entry.UserId = userId + entry.DeviceId = deviceId + entry.SessionId = sessionId + entry.ClientAppName = clientAppName + entry.ClientAppVersion = clientAppVersion + entry.AuthLevel = authLevel + entry.Priority = priority + entry.RequestHeaders = reqHeaders + entry.RequestData = reqData + entry.ResponseCode = responseCode + entry.UsedTime = usedTime + entry.ResponseHeaders = respHeaders + entry.ResponseData = responseData + entry.ResponseDataLength = responseDataLength + if len(extra) > 0 { + cast.FillMap(&entry.Extra, extra) + } + + logger.Log(entry) +} + +type TaskLog struct { + log.BaseLog + Task string `log:"pos:6"` + UsedTime float32 `log:"pos:7"` + Success bool `log:"pos:8"` + Message string `log:"pos:9"` +} + +func (l *TaskLog) Reset() { + l.BaseLog.Reset() + l.Task = "" + l.UsedTime = 0 + l.Success = false + l.Message = "" +} + +type MonitorLog struct { + log.BaseLog + Target string `log:"pos:6"` + Status int `log:"pos:7"` + Message string `log:"pos:8"` +} + +func (l *MonitorLog) Reset() { + l.BaseLog.Reset() + l.Target = "" + l.Status = 0 + l.Message = "" +} + +type StatisticLog struct { + log.BaseLog + Category string `log:"pos:6"` + Item string `log:"pos:7"` + Value float64 `log:"pos:8"` +} + +func (l *StatisticLog) Reset() { + l.BaseLog.Reset() + l.Category = "" + l.Item = "" + l.Value = 0 +} + +func logTask(logger *log.Logger, taskName string, usedTime float32, success bool, message string, extra ...any) { + if logger.CheckLevel(log.INFO) { + entry := log.GetEntry[TaskLog]() + logger.FillBase(entry.GetBaseLog(), log.LogTypeTask) + entry.Task = taskName + entry.UsedTime = usedTime + entry.Success = success + entry.Message = message + if len(extra) > 0 { + cast.FillMap(&entry.Extra, extra) + } + logger.Log(entry) + } +} + +func logMonitor(logger *log.Logger, target string, status int, message string, extra ...any) { + if logger.CheckLevel(log.INFO) { + entry := log.GetEntry[MonitorLog]() + logger.FillBase(entry.GetBaseLog(), log.LogTypeMonitor) + entry.Target = target + entry.Status = status + entry.Message = message + if len(extra) > 0 { + cast.FillMap(&entry.Extra, extra) + } + logger.Log(entry) + } +} + +func logStatistic(logger *log.Logger, category, item string, value float64, extra ...any) { + if logger.CheckLevel(log.INFO) { + entry := log.GetEntry[StatisticLog]() + logger.FillBase(entry.GetBaseLog(), log.LogTypeStatistic) + entry.Category = category + entry.Item = item + entry.Value = value + if len(extra) > 0 { + cast.FillMap(&entry.Extra, extra) + } + logger.Log(entry) + } +} + +func init() { + log.RegisterType(log.LogTypeRequest, &RequestLog{}) + log.RegisterType(log.LogTypeTask, &TaskLog{}) + log.RegisterType(log.LogTypeMonitor, &MonitorLog{}) + log.RegisterType(log.LogTypeStatistic, &StatisticLog{}) +} diff --git a/proxy.go b/proxy.go index 969c923..5ff70f2 100644 --- a/proxy.go +++ b/proxy.go @@ -9,11 +9,10 @@ import ( "net/http" "regexp" "strings" - "sync" "time" ) -type proxyInfo struct { +type proxyType struct { matcher *regexp.Regexp authLevel int fromPath string @@ -21,39 +20,32 @@ type proxyInfo struct { toPath string } -var ( - proxies = make(map[string]*proxyInfo) - regexProxies = make([]*proxyInfo, 0) - proxyBy func(*Request) (int, *string, *string, map[string]string) - proxiesLock = sync.RWMutex{} - - httpClientPool *gohttp.Client -) - -// Proxy 注册代理规则 -func Proxy(authLevel int, path string, toApp, toPath string) { - p := &proxyInfo{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath} +func (hc *HostContext) Proxy(authLevel int, path string, toApp, toPath string) *HostContext { + p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath} if strings.Contains(path, "(") { matcher, err := regexp.Compile("^" + path + "$") if err == nil { p.matcher = matcher - proxiesLock.Lock() - regexProxies = append(regexProxies, p) - proxiesLock.Unlock() } - } else { - proxiesLock.Lock() - proxies[path] = p - proxiesLock.Unlock() } + + hostPoliciesLock.Lock() + defer hostPoliciesLock.Unlock() + hostProxies[hc.host] = append(hostProxies[hc.host], p) + return hc } -// SetProxyBy 设置动态代理函数 -func SetProxyBy(by func(request *Request) (authLevel int, toApp, toPath *string, headers map[string]string)) { - proxyBy = by -} +var httpClientPool *gohttp.Client + +func findProxy(request *Request) (int, *string, *string, string) { + host := request.Host + hostOnly, port, _ := strings.Cut(host, ":") + hosts := []string{host} + if port != "" { + hosts = append(hosts, hostOnly, ":"+port) + } + hosts = append(hosts, "*") -func findProxy(request *Request) (int, *string, *string) { requestPath := request.RequestURI queryString := "" if pos := strings.Index(requestPath, "?"); pos != -1 { @@ -61,40 +53,42 @@ func findProxy(request *Request) (int, *string, *string) { requestPath = requestPath[:pos] } - proxiesLock.RLock() - defer proxiesLock.RUnlock() + hostPoliciesLock.RLock() + defer hostPoliciesLock.RUnlock() - if pi, ok := proxies[requestPath]; ok { - toPath := pi.toPath + queryString - return pi.authLevel, &pi.toApp, &toPath - } + for _, h := range hosts { + proxies, exists := hostProxies[h] + if !exists { + continue + } - for _, pi := range regexProxies { - if pi.matcher != nil { - finds := pi.matcher.FindAllStringSubmatch(requestPath, 1) - if len(finds) > 0 { - toApp := pi.toApp - toPath := pi.toPath - for i, part := range finds[0] { - toApp = strings.ReplaceAll(toApp, fmt.Sprintf("$%d", i), part) - toPath = strings.ReplaceAll(toPath, fmt.Sprintf("$%d", i), part) + for _, pi := range proxies { + if pi.matcher == nil { + if pi.fromPath == requestPath { + toPath := pi.toPath + queryString + return pi.authLevel, &pi.toApp, &toPath, h + } + } else { + finds := pi.matcher.FindAllStringSubmatch(requestPath, 1) + if len(finds) > 0 { + toApp := pi.toApp + toPath := pi.toPath + for i, part := range finds[0] { + toApp = strings.ReplaceAll(toApp, fmt.Sprintf("$%d", i), part) + toPath = strings.ReplaceAll(toPath, fmt.Sprintf("$%d", i), part) + } + toPath += queryString + return pi.authLevel, &toApp, &toPath, h } - toPath += queryString - return pi.authLevel, &toApp, &toPath } } } - return 0, nil, nil + return 0, nil, nil, "" } func processProxy(request *Request, response *Response, logger *log.Logger) bool { - authLevel, proxyToApp, proxyToPath := findProxy(request) - var proxyHeaders map[string]string - - if proxyBy != nil && (proxyToApp == nil || proxyToPath == nil || *proxyToApp == "" || *proxyToPath == "") { - authLevel, proxyToApp, proxyToPath, proxyHeaders = proxyBy(request) - } + authLevel, proxyToApp, proxyToPath, foundHost := findProxy(request) if proxyToApp == nil || proxyToPath == nil || *proxyToApp == "" || *proxyToPath == "" { return false @@ -112,25 +106,20 @@ func processProxy(request *Request, response *Response, logger *log.Logger) bool app := *proxyToApp path := *proxyToPath - - // 构建自定义头部 - headerArgs := make([]string, 0) - for k, v := range proxyHeaders { - headerArgs = append(headerArgs, k, v) - } + logger.Info("proxy", "app", app, "path", path, "host", foundHost) if strings.Contains(app, "://") { // 直接 URL 代理 if httpClientPool == nil { httpClientPool = gohttp.NewClient(time.Duration(Config.RedirectTimeout) * time.Millisecond) } - res := httpClientPool.ManualDoByRequest(request.Request, request.Method, app+path, request.Body, headerArgs...) + res := httpClientPool.ManualDoByRequest(request.Request, request.Method, app+path, request.Body) copyResponse(res, response, logger) } else { // Discover 代理 caller := discover.NewCaller(request.Request, logger) caller.NoBody = true - res, _ := caller.ManualDoWithNode(request.Method, app, "", path, request.Body, headerArgs...) + res, _ := caller.ManualDoWithNode(request.Method, app, "", path, request.Body) copyResponse(res, response, logger) } diff --git a/proxy_test.go b/proxy_test.go index 5f177b6..9ae6e26 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -8,14 +8,14 @@ import ( func TestRewrite(t *testing.T) { // 注册重写规则 - Rewrite("/old", "/new") - Rewrite("/regex/(.*)", "/target/$1") + Host("*").Rewrite("/old", "/new") + Host("*").Rewrite("/regex/(.*)", "/target/$1") // 注册目标服务 - Register(0, "/new", func() string { return "new content" }, "new") - Register(0, "/target/123", func() string { return "target content" }, "target") + Host("*").ANY("/new", func() string { return "new content" }).Memo("new") + Host("*").ANY("/target/123", func() string { return "target content" }).Memo("target") - rh := &routeHandler{} + rh := &RouteHandler{} // 测试精确匹配重写 req1 := httptest.NewRequest("GET", "/old", nil) @@ -43,9 +43,9 @@ func TestProxyDirect(t *testing.T) { defer backend.Close() // 注册代理规则 - Proxy(0, "/proxy", backend.URL, "/hello") + Host("*").Proxy(0, "/proxy", backend.URL, "/hello") - rh := &routeHandler{} + rh := &RouteHandler{} req := httptest.NewRequest("GET", "/proxy", nil) w := httptest.NewRecorder() rh.ServeHTTP(w, req) diff --git a/request.go b/request.go index 800a9ad..9e111f1 100644 --- a/request.go +++ b/request.go @@ -1,16 +1,14 @@ package service import ( - "apigo.cc/go/cast" - "apigo.cc/go/standard" + "apigo.cc/go/discover" + "apigo.cc/go/file" "io" "mime/multipart" "net" "net/http" "net/textproto" "net/url" - "os" - "path/filepath" ) // UploadFile 上传文件结构 @@ -28,25 +26,11 @@ func (f *UploadFile) Open() (multipart.File, error) { // Save 保存上传文件到本地 func (f *UploadFile) Save(filename string) error { - dir := filepath.Dir(filename) - if _, err := os.Stat(dir); os.IsNotExist(err) { - _ = os.MkdirAll(dir, 0755) - } - - dst, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + data, err := f.Content() if err != nil { return err } - defer dst.Close() - - src, err := f.fileHeader.Open() - if err != nil { - return err - } - defer src.Close() - - _, err = io.Copy(dst, src) - return err + return file.WriteBytes(filename, data) } // Content 获取上传文件内容 @@ -94,34 +78,37 @@ func (r *Request) Get(key string) any { // MakeUrl 根据当前请求构建完整 URL func (r *Request) MakeUrl(path string) string { - scheme := r.Header.Get(standard.DiscoverHeaderScheme) + scheme := r.Header.Get(discover.HeaderScheme) if scheme == "" { scheme = "http" } - host := r.Header.Get(standard.DiscoverHeaderHost) + host := r.Header.Get(discover.HeaderHost) if host == "" { host = r.Host } return scheme + "://" + host + path } -// GetSessionId 获取会话 ID -func (r *Request) GetSessionId() string { - sessionId := r.Header.Get(Config.Listen) // Wait, this should be usedSessionIdKey - // TODO: Fix dependency on global usedSessionIdKey - return sessionId +// DeviceId 获取设备 ID +func (r *Request) DeviceId() string { + return r.Header.Get(discover.HeaderDeviceID) +} + +// SessionId 获取会话 ID +func (r *Request) SessionId() string { + return r.Header.Get(discover.HeaderSessionID) } // SetUserId 设置用户 ID(传递给下游) func (r *Request) SetUserId(userId string) { - r.Header.Set(standard.DiscoverHeaderUserId, userId) + r.Header.Set(discover.HeaderUserID, userId) } -// GetRealIp 获取真实 IP -func (r *Request) GetRealIp() string { - ip := r.Header.Get(standard.DiscoverHeaderClientIp) +// ClientIp 获取真实 IP +func (r *Request) ClientIp() string { + ip := r.Header.Get(discover.HeaderClientIP) if ip == "" { - ip = r.Header.Get(standard.DiscoverHeaderForwardedFor) + ip = r.Header.Get(discover.HeaderForwardedFor) } if ip == "" { host, _, err := net.SplitHostPort(r.RemoteAddr) @@ -132,8 +119,3 @@ func (r *Request) GetRealIp() string { } return ip } - -// GetLowerName (Aliased from cast) -func GetLowerName(s string) string { - return cast.GetLowerName(s) -} diff --git a/response.go b/response.go index e51d926..b8a6d60 100644 --- a/response.go +++ b/response.go @@ -2,16 +2,17 @@ package service import ( "apigo.cc/go/cast" + "apigo.cc/go/file" "io" "net/http" - "os" ) // Response 封装 http.ResponseWriter type Response struct { Id string Writer http.ResponseWriter - status int + Code int + body []byte outLen int changed bool headerWritten bool @@ -24,7 +25,7 @@ type Response struct { func NewResponse(writer http.ResponseWriter) *Response { return &Response{ Writer: writer, - status: http.StatusOK, + Code: http.StatusOK, } } @@ -42,6 +43,9 @@ func (r *Response) Write(bytes []byte) (int, error) { r.checkWriteHeader() r.changed = true r.outLen += len(bytes) + if r.Code != http.StatusOK && len(r.body) < 4096 { + r.body = append(r.body, bytes...) + } if r.ProxyHeader != nil { r.copyProxyHeader() } @@ -56,8 +60,8 @@ func (r *Response) WriteString(s string) (int, error) { // WriteHeader 设置响应状态码 func (r *Response) WriteHeader(code int) { r.changed = true - r.status = code - if r.ProxyHeader != nil && (r.status == http.StatusBadGateway || r.status == http.StatusServiceUnavailable || r.status == http.StatusGatewayTimeout) { + r.Code = code + if r.ProxyHeader != nil && (r.Code == http.StatusBadGateway || r.Code == http.StatusServiceUnavailable || r.Code == http.StatusGatewayTimeout) { return } if r.ProxyHeader != nil { @@ -68,8 +72,8 @@ func (r *Response) WriteHeader(code int) { func (r *Response) checkWriteHeader() { if !r.headerWritten { r.headerWritten = true - if r.status != http.StatusOK { - r.Writer.WriteHeader(r.status) + if r.Code != http.StatusOK { + r.Writer.WriteHeader(r.Code) } } } @@ -94,7 +98,7 @@ func (r *Response) Flush() { // GetStatusCode 获取当前状态码 func (r *Response) GetStatusCode() int { - return r.status + return r.Code } // DontLog200 标记不记录 200 状态码的日志 @@ -111,10 +115,8 @@ func (r *Response) Location(location string) { // SendFile 发送文件 func (r *Response) SendFile(contentType, filename string) { r.Header().Set("Content-Type", contentType) - // TODO: Integrate memory file support if needed - if fd, err := os.Open(filename); err == nil { - defer fd.Close() - _, _ = io.Copy(r, fd) + if data, err := file.ReadBytes(filename); err == nil { + _, _ = r.Write(data) } } diff --git a/rewrite.go b/rewrite.go index 70d340b..35c9cb1 100644 --- a/rewrite.go +++ b/rewrite.go @@ -6,47 +6,42 @@ import ( "net/url" "regexp" "strings" - "sync" ) -type rewriteInfo struct { +type rewriteType struct { matcher *regexp.Regexp fromPath string toPath string } -var ( - rewrites = make(map[string]*rewriteInfo) - regexRewrites = make([]*rewriteInfo, 0) - rewriteBy func(*Request) (string, bool) - rewritesLock = sync.RWMutex{} -) - -// Rewrite 注册重写规则 -func Rewrite(path string, toPath string) { - s := &rewriteInfo{fromPath: path, toPath: toPath} +func (hc *HostContext) Rewrite(path string, toPath string) *HostContext { + s := &rewriteType{fromPath: path, toPath: toPath} if strings.ContainsRune(path, '(') { matcher, err := regexp.Compile("^" + path + "$") if err == nil { s.matcher = matcher - rewritesLock.Lock() - regexRewrites = append(regexRewrites, s) - rewritesLock.Unlock() } - } else { - rewritesLock.Lock() - rewrites[path] = s - rewritesLock.Unlock() } -} -// SetRewriteBy 设置动态重写函数 -func SetRewriteBy(by func(request *Request) (toPath string, rewrite bool)) { - rewriteBy = by + hostPoliciesLock.Lock() + defer hostPoliciesLock.Unlock() + hostRewrites[hc.host] = append(hostRewrites[hc.host], s) + return hc } func processRewrite(request *Request, response *Response, logger *log.Logger) bool { + host := request.Host + hostOnly, port, _ := strings.Cut(host, ":") + hosts := []string{host} + if port != "" { + hosts = append(hosts, hostOnly, ":"+port) + } + hosts = append(hosts, "*") + + hostPoliciesLock.RLock() + defer hostPoliciesLock.RUnlock() + requestPath := request.RequestURI queryString := "" if pos := strings.Index(requestPath, "?"); pos != -1 { @@ -54,25 +49,22 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo requestPath = requestPath[:pos] } - var rewriteToPath string - var found bool + for _, h := range hosts { + rewrites, exists := hostRewrites[h] + if !exists { + continue + } - rewritesLock.RLock() - // 1. 精确匹配 - if ri, ok := rewrites[requestPath]; ok { - rewriteToPath = ri.toPath - found = true - } + for _, ri := range rewrites { + found := false + rewriteToPath := "" - // 2. 动态重写 - if !found && rewriteBy != nil { - rewriteToPath, found = rewriteBy(request) - } - - // 3. 正则匹配 - if !found { - for _, ri := range regexRewrites { - if ri.matcher != nil { + if ri.matcher == nil { + if ri.fromPath == requestPath { + rewriteToPath = ri.toPath + found = true + } + } else { finds := ri.matcher.FindAllStringSubmatch(request.RequestURI, 1) if len(finds) > 0 { toPath := ri.toPath @@ -81,31 +73,29 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo } rewriteToPath = toPath found = true - break } } - } - } - rewritesLock.RUnlock() - if found { - if strings.Contains(rewriteToPath, "://") { - // 外部重定向 - if !strings.Contains(rewriteToPath, "?") && queryString != "" { - rewriteToPath += queryString + if found { + if strings.Contains(rewriteToPath, "://") { + // 外部重定向 + if !strings.Contains(rewriteToPath, "?") && queryString != "" { + rewriteToPath += queryString + } + response.Header().Set("Location", rewriteToPath) + response.WriteHeader(302) + return true + } else { + // 内部重写 + logger.Info("rewrite", "from", request.RequestURI, "to", rewriteToPath, "host", h) + if queryString != "" && !strings.Contains(rewriteToPath, "?") { + rewriteToPath += queryString + } + request.RequestURI = rewriteToPath + request.URL, _ = url.Parse(rewriteToPath) + return false // 继续后续处理 + } } - response.Header().Set("Location", rewriteToPath) - response.WriteHeader(302) - return true - } else { - // 内部重写 - logger.Info("rewrite", "from", request.RequestURI, "to", rewriteToPath) - if queryString != "" && !strings.Contains(rewriteToPath, "?") { - rewriteToPath += queryString - } - request.RequestURI = rewriteToPath - request.URL, _ = url.Parse(rewriteToPath) - return false // 继续后续处理 } } diff --git a/server.go b/server.go index 28a63b3..28ed375 100644 --- a/server.go +++ b/server.go @@ -50,7 +50,7 @@ func (as *AsyncServer) start() { serverAddr = as.Addr as.server = &http.Server{ - Handler: &routeHandler{}, + Handler: &RouteHandler{}, } signal.Notify(as.stopChan, os.Interrupt, syscall.SIGTERM) diff --git a/service.go b/service.go index bbe48fe..61fc66b 100644 --- a/service.go +++ b/service.go @@ -9,31 +9,15 @@ import ( "sync" ) -// Map 通用 Map 类型 -type Map = map[string]any - -// Arr 通用切片类型 -type Arr = []any - -// WebServiceOptions 服务注册选项 -type WebServiceOptions struct { - Priority int - NoDoc bool - NoBody bool - NoLog200 bool - Host string - Ext Map - // Limiters []*Limiter // TODO: Integrate Limiter -} - // webServiceType 内部存储的服务元数据 type webServiceType struct { authLevel int method string + host string path string pathMatcher *regexp.Regexp pathArgs []string - parmsNum int + paramsNum int inType reflect.Type inIndex int headersType reflect.Type @@ -47,10 +31,29 @@ type webServiceType struct { funcType reflect.Type funcValue reflect.Value options WebServiceOptions - data Map + data map[string]any memo string } +// WebServiceOptions 服务注册选项 +type WebServiceOptions struct { + Priority int + NoDoc bool + NoBody bool + NoLog200 bool + Ext map[string]any +} + +type websocketServiceType struct { + authLevel int + host string + path string + memo string + funcType reflect.Type + funcValue reflect.Value + options WebServiceOptions +} + var ( serverId string serverAddr string @@ -58,14 +61,21 @@ var ( serverProtoName = "http" running = false - webServices = make(map[string]*webServiceType) - regexWebServices = make([]*webServiceType, 0) + // webServices 按 Host 隔离: map[host]map[method+path]*webServiceType + webServices = make(map[string]map[string]*webServiceType) + // regexWebServices 按 Host 隔离: map[host][]*webServiceType + regexWebServices = make(map[string][]*webServiceType) webServicesLock = sync.RWMutex{} webServicesList = make([]*webServiceType, 0) - websocketServices = make(map[string]*websocketServiceType) - websocketServicesLock = sync.RWMutex{} - websocketServicesList = make([]*webServiceType, 0) + websocketServices = make(map[string]map[string]*websocketServiceType) + websocketServicesLock = sync.RWMutex{} + websocketServicesList = make([]*websocketServiceType, 0) + + // Rewrite 与 Proxy 按 Host 隔离 + hostRewrites = make(map[string][]*rewriteType) + hostProxies = make(map[string][]*proxyType) + hostPoliciesLock = sync.RWMutex{} // 过滤器与拦截器 inFilters = make([]func(*map[string]any, *Request, *Response, *log.Logger) any, 0) @@ -118,29 +128,28 @@ func SetOutFilter(filter func(in map[string]any, request *Request, response *Res outFilters = append(outFilters, filter) } -// Register 注册服务(通用方法) -func Register(authLevel int, path string, serviceFunc any, memo string) { - Restful(authLevel, "", path, serviceFunc, memo) +// HostContext 提供流式服务注册能力 +type HostContext struct { + host string } -// Restful 注册指定方法的服务 -func Restful(authLevel int, method, path string, serviceFunc any, memo string) { - RestfulWithOptions(authLevel, method, path, serviceFunc, memo, WebServiceOptions{}) +// Host 指定服务运行的 Host (支持 "example.com", ":8080", "example.com:8080", "*") +func Host(host string) *HostContext { + if host == "" { + host = "*" + } + return &HostContext{host: host} } -// RestfulWithOptions 注册带选项的服务 -func RestfulWithOptions(authLevel int, method, path string, serviceFunc any, memo string, options WebServiceOptions) { +func (hc *HostContext) Register(method, path string, serviceFunc any) *webServiceType { s, err := makeCachedService(serviceFunc) if err != nil { - // TODO: Log error properly when logger is ready - return + return &webServiceType{} // 返回空对象避免链式调用崩溃 } - s.authLevel = authLevel - s.options = options - s.method = method + s.host = hc.host + s.method = strings.ToUpper(method) s.path = path - s.memo = memo // 解析路径参数 {name} finder, err := regexp.Compile("{(.*?)}") @@ -159,13 +168,169 @@ func RestfulWithOptions(authLevel int, method, path string, serviceFunc any, mem webServicesLock.Lock() defer webServicesLock.Unlock() - // 简单路径匹配 if s.pathMatcher == nil { - webServices[method+path] = s // TODO: Include Host in key + if webServices[s.host] == nil { + webServices[s.host] = make(map[string]*webServiceType) + } + webServices[s.host][s.method+s.path] = s } else { - regexWebServices = append(regexWebServices, s) + regexWebServices[s.host] = append(regexWebServices[s.host], s) } webServicesList = append(webServicesList, s) + return s +} + +func (hc *HostContext) GET(path string, serviceFunc any) *webServiceType { + return hc.Register("GET", path, serviceFunc) +} + +func (hc *HostContext) POST(path string, serviceFunc any) *webServiceType { + return hc.Register("POST", path, serviceFunc) +} + +func (hc *HostContext) PUT(path string, serviceFunc any) *webServiceType { + return hc.Register("PUT", path, serviceFunc) +} + +func (hc *HostContext) DELETE(path string, serviceFunc any) *webServiceType { + return hc.Register("DELETE", path, serviceFunc) +} + +func (hc *HostContext) PATCH(path string, serviceFunc any) *webServiceType { + return hc.Register("PATCH", path, serviceFunc) +} + +func (hc *HostContext) HEAD(path string, serviceFunc any) *webServiceType { + return hc.Register("HEAD", path, serviceFunc) +} + +func (hc *HostContext) OPTIONS(path string, serviceFunc any) *webServiceType { + return hc.Register("OPTIONS", path, serviceFunc) +} + +func (hc *HostContext) ANY(path string, serviceFunc any) *webServiceType { + return hc.Register("*", path, serviceFunc) +} + +// GroupContext 提供路径分组注册能力 +type GroupContext struct { + hc *HostContext + prefix string +} + +// Group 创建路径分组 +func (hc *HostContext) Group(prefix string) *GroupContext { + if prefix == "/" { + prefix = "" + } + return &GroupContext{hc: hc, prefix: prefix} +} + +func (gc *GroupContext) GET(path string, serviceFunc any) *webServiceType { + return gc.hc.Register("GET", gc.prefix+path, serviceFunc) +} + +func (gc *GroupContext) POST(path string, serviceFunc any) *webServiceType { + return gc.hc.Register("POST", gc.prefix+path, serviceFunc) +} + +func (gc *GroupContext) PUT(path string, serviceFunc any) *webServiceType { + return gc.hc.Register("PUT", gc.prefix+path, serviceFunc) +} + +func (gc *GroupContext) DELETE(path string, serviceFunc any) *webServiceType { + return gc.hc.Register("DELETE", gc.prefix+path, serviceFunc) +} + +func (gc *GroupContext) ANY(path string, serviceFunc any) *webServiceType { + return gc.hc.Register("*", gc.prefix+path, serviceFunc) +} + +func (gc *GroupContext) WebSocket(path string, serviceFunc any) *websocketServiceType { + return gc.hc.WebSocket(gc.prefix+path, serviceFunc) +} + +func (gc *GroupContext) Rewrite(path string, toPath string) *GroupContext { + gc.hc.Rewrite(gc.prefix+path, toPath) + return gc +} + +func (gc *GroupContext) Proxy(authLevel int, path string, toApp, toPath string) *GroupContext { + gc.hc.Proxy(authLevel, gc.prefix+path, toApp, toPath) + return gc +} + +func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType { + funcType := reflect.TypeOf(serviceFunc) + if funcType.Kind() != reflect.Func { + return &websocketServiceType{} + } + + ws := &websocketServiceType{ + host: hc.host, + path: path, + funcType: funcType, + funcValue: reflect.ValueOf(serviceFunc), + } + + websocketServicesLock.Lock() + defer websocketServicesLock.Unlock() + if websocketServices[hc.host] == nil { + websocketServices[hc.host] = make(map[string]*websocketServiceType) + } + websocketServices[hc.host][path] = ws + websocketServicesList = append(websocketServicesList, ws) + return ws +} + +// webServiceType 链式配置方法 +func (s *webServiceType) Auth(level int) *webServiceType { + s.authLevel = level + return s +} + +func (s *webServiceType) Memo(memo string) *webServiceType { + s.memo = memo + return s +} + +func (s *webServiceType) Priority(p int) *webServiceType { + s.options.Priority = p + return s +} + +func (s *webServiceType) NoDoc() *webServiceType { + s.options.NoDoc = true + return s +} + +func (s *webServiceType) NoBody() *webServiceType { + s.options.NoBody = true + return s +} + +func (s *webServiceType) NoLog200() *webServiceType { + s.options.NoLog200 = true + return s +} + +func (s *webServiceType) Ext(key string, val any) *webServiceType { + if s.options.Ext == nil { + s.options.Ext = make(map[string]any) + } + s.options.Ext[key] = val + return s +} + +// websocketServiceType 链式配置方法 +func (s *websocketServiceType) Auth(level int) *websocketServiceType { + s.authLevel = level + return s +} + +func (s *websocketServiceType) Memo(memo string) *websocketServiceType { + s.memo = memo + return s } func makeCachedService(matchedService any) (*webServiceType, error) { @@ -175,7 +340,7 @@ func makeCachedService(matchedService any) (*webServiceType, error) { } targetService := &webServiceType{ - parmsNum: funcType.NumIn(), + paramsNum: funcType.NumIn(), inIndex: -1, headersIndex: -1, requestIndex: -1, @@ -188,7 +353,7 @@ func makeCachedService(matchedService any) (*webServiceType, error) { funcValue: reflect.ValueOf(matchedService), } - for i := 0; i < targetService.parmsNum; i++ { + for i := 0; i < targetService.paramsNum; i++ { t := funcType.In(i) tStr := t.String() switch tStr { @@ -228,3 +393,14 @@ func GetInject(dataType reflect.Type) any { } return nil } + +// GetInjectT 获取注入对象 (泛型版) +func GetInjectT[T any]() T { + var zero T + t := reflect.TypeOf((*T)(nil)).Elem() + obj := GetInject(t) + if obj == nil { + return zero + } + return obj.(T) +} diff --git a/service_test.go b/service_test.go index 1e4c400..d24f60d 100644 --- a/service_test.go +++ b/service_test.go @@ -10,10 +10,10 @@ func TestServiceRegister(t *testing.T) { return "ok" } - Register(0, "/test", handler, "test service") + Host("*").Register("*", "/test", handler).Auth(0).Memo("test service") webServicesLock.RLock() - s := webServices["/test"] + s := webServices["*"]["*/test"] webServicesLock.RUnlock() if s == nil { @@ -33,16 +33,21 @@ func TestRegexServiceRegister(t *testing.T) { return "ok" } - Register(0, "/user/{id}", handler, "get user") + Host("*").Register("*", "/user/{id}", handler).Auth(0).Memo("get user") webServicesLock.RLock() found := false - for _, s := range regexWebServices { - if s.path == "/user/{id}" { - found = true - if len(s.pathArgs) != 1 || s.pathArgs[0] != "id" { - t.Errorf("pathArgs mismatch: %v", s.pathArgs) + for _, services := range regexWebServices { + for _, s := range services { + if s.path == "/user/{id}" { + found = true + if len(s.pathArgs) != 1 || s.pathArgs[0] != "id" { + t.Errorf("pathArgs mismatch: %v", s.pathArgs) + } + break } + } + if found { break } } diff --git a/static.go b/static.go index 5d33d67..ab3db34 100644 --- a/static.go +++ b/static.go @@ -5,7 +5,6 @@ import ( "apigo.cc/go/log" "mime" "net/http" - "os" "path/filepath" "strings" "sync" @@ -74,16 +73,16 @@ func processStatic(requestPath string, request *Request, response *Response, log return false } - info, err := os.Stat(filePath) - if err != nil { + info := file.GetFileInfo(filePath) + if info == nil { return false } - if info.IsDir() { + if info.IsDir { // 自动查找索引文件 for _, indexFile := range Config.IndexFiles { f := filepath.Join(filePath, indexFile) - if i, err := os.Stat(f); err == nil && !i.IsDir() { + if i := file.GetFileInfo(f); i != nil && !i.IsDir { filePath = f info = i break @@ -91,14 +90,15 @@ func processStatic(requestPath string, request *Request, response *Response, log } } - if info.IsDir() { + if info.IsDir { return false } // 检查 304 if ifModifiedSince := request.Header.Get("If-Modified-Since"); ifModifiedSince != "" { if t, err := time.Parse(http.TimeFormat, ifModifiedSince); err == nil { - if !info.ModTime().Truncate(time.Second).After(t.Truncate(time.Second)) { + if time.Unix(info.ModTime, 0).Truncate(time.Second).Before(t.Truncate(time.Second)) || + time.Unix(info.ModTime, 0).Truncate(time.Second).Equal(t.Truncate(time.Second)) { response.WriteHeader(http.StatusNotModified) return true } @@ -111,7 +111,7 @@ func processStatic(requestPath string, request *Request, response *Response, log contentType = "application/octet-stream" } response.Header().Set("Content-Type", contentType) - response.Header().Set("Last-Modified", info.ModTime().UTC().Format(http.TimeFormat)) + response.Header().Set("Last-Modified", time.Unix(info.ModTime, 0).UTC().Format(http.TimeFormat)) data, err := file.ReadBytes(filePath) if err != nil { diff --git a/static_test.go b/static_test.go index 20a6056..8eb330c 100644 --- a/static_test.go +++ b/static_test.go @@ -19,7 +19,7 @@ func TestStaticService(t *testing.T) { // 注册静态目录 Static("/ui", tempDir) - rh := &routeHandler{} + rh := &RouteHandler{} // 测试成功访问 req := httptest.NewRequest("GET", "/ui/index.html", nil) diff --git a/verify.go b/verify.go index bce9bd0..1417d67 100644 --- a/verify.go +++ b/verify.go @@ -92,7 +92,7 @@ func VerifyStruct(in any, logger *log.Logger) (ok bool, field string) { keyTag := ft.Tag.Get("verifyKey") if tag != "" || keyTag != "" { var err error - ok, f, err := _verifyValue(fv, tag, keyTag, logger) + ok, f, err := verifyValue(fv, tag, keyTag, logger) if !ok { if f == "" { f = cast.GetLowerName(ft.Name) @@ -111,13 +111,13 @@ func VerifyStruct(in any, logger *log.Logger) (ok bool, field string) { return true, "" } -func _verifyValue(in reflect.Value, setting, keySetting string, logger *log.Logger) (bool, string, error) { +func verifyValue(in reflect.Value, setting, keySetting string, logger *log.Logger) (bool, string, error) { t := in.Type() // 处理切片 (非 byte 切片) if t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8 { if setting != "" { for i := 0; i < in.Len(); i++ { - if ok, f, err := _verifyValue(in.Index(i), setting, "", logger); !ok { + if ok, f, err := verifyValue(in.Index(i), setting, "", logger); !ok { return false, f, err } } @@ -129,12 +129,12 @@ func _verifyValue(in reflect.Value, setting, keySetting string, logger *log.Logg if t.Kind() == reflect.Map { for _, k := range in.MapKeys() { if keySetting != "" { - if ok, _, err := _verifyValue(k, keySetting, "", logger); !ok { + if ok, _, err := verifyValue(k, keySetting, "", logger); !ok { return false, "key", err } } if setting != "" { - if ok, f, err := _verifyValue(in.MapIndex(k), setting, "", logger); !ok { + if ok, f, err := verifyValue(in.MapIndex(k), setting, "", logger); !ok { return false, f, err } } diff --git a/websocket.go b/websocket.go index 4b8b770..91f67f9 100644 --- a/websocket.go +++ b/websocket.go @@ -7,40 +7,12 @@ import ( "reflect" ) -// websocketServiceType WebSocket 服务元数据 -type websocketServiceType struct { - authLevel int - path string - updater *websocket.Upgrader - handlerValue reflect.Value - handlerType reflect.Type - memo string -} - -// RegisterWebsocket 注册 WebSocket 服务 -func RegisterWebsocket(authLevel int, path string, handler any, memo string) { - v := reflect.ValueOf(handler) - t := v.Type() - if t.Kind() != reflect.Func { - return - } - - s := &websocketServiceType{ - authLevel: authLevel, - path: path, - memo: memo, - updater: &websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}, - handlerValue: v, - handlerType: t, - } - - websocketServicesLock.Lock() - websocketServices[path] = s - websocketServicesLock.Unlock() +var defaultUpgrader = &websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, } func doWebsocketService(ws *websocketServiceType, request *Request, response *Response, logger *log.Logger) { - conn, err := ws.updater.Upgrade(response.Writer, request.Request, nil) + conn, err := defaultUpgrader.Upgrade(response.Writer, request.Request, nil) if err != nil { logger.Error("websocket upgrade failed", "error", err.Error()) return @@ -48,9 +20,9 @@ func doWebsocketService(ws *websocketServiceType, request *Request, response *Re defer conn.Close() // 调用业务处理函数,注入依赖 - params := make([]reflect.Value, ws.handlerType.NumIn()) + params := make([]reflect.Value, ws.funcType.NumIn()) for i := 0; i < len(params); i++ { - t := ws.handlerType.In(i) + t := ws.funcType.In(i) if t == reflect.TypeOf(request) { params[i] = reflect.ValueOf(request) } else if t == reflect.TypeOf(logger) { @@ -63,5 +35,5 @@ func doWebsocketService(ws *websocketServiceType, request *Request, response *Re params[i] = reflect.New(t).Elem() } } - ws.handlerValue.Call(params) + ws.funcValue.Call(params) } diff --git a/websocket_test.go b/websocket_test.go index 2456702..1d0ff4d 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -9,18 +9,18 @@ import ( func TestWebSocketService(t *testing.T) { // 注册 WebSocket 服务 - RegisterWebsocket(0, "/ws", func(conn *websocket.Conn) { + Host("*").WebSocket("/ws", func(conn *websocket.Conn) { for { - var msg Map + var msg map[string]any if err := conn.ReadJSON(&msg); err != nil { break } - _ = conn.WriteJSON(Map{"reply": msg["msg"]}) + _ = conn.WriteJSON(map[string]any{"reply": msg["msg"]}) } - }, "test websocket") + }).Auth(0).Memo("test websocket") // 启动测试服务器 - server := httptest.NewServer(&routeHandler{}) + server := httptest.NewServer(&RouteHandler{}) defer server.Close() // 建立连接 @@ -32,13 +32,13 @@ func TestWebSocketService(t *testing.T) { defer conn.Close() // 发送消息 - msg := Map{"action": "echo", "msg": "hello"} + msg := map[string]any{"action": "echo", "msg": "hello"} if err := conn.WriteJSON(msg); err != nil { t.Fatalf("WriteJSON failed: %v", err) } // 接收响应 - var reply Map + var reply map[string]any if err := conn.ReadJSON(&reply); err != nil { t.Fatalf("ReadJSON failed: %v", err) }