feat: implement semantic ID, fine-grained logging, and SOP alignment (by AI)
This commit is contained in:
parent
0ff3b47156
commit
fec9c40f93
347
.log.meta.json
347
.log.meta.json
@ -436,118 +436,6 @@
|
|||||||
"Hide": 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": [
|
"request": [
|
||||||
{
|
{
|
||||||
"Index": 0,
|
"Index": 0,
|
||||||
@ -902,241 +790,6 @@
|
|||||||
"Hide": 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": [
|
"warning": [
|
||||||
{
|
{
|
||||||
"Index": 0,
|
"Index": 0,
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
# CHANGELOG - go/service
|
# CHANGELOG - go/service
|
||||||
|
|
||||||
|
## v1.0.4 (2026-05-10)
|
||||||
|
- **Log Optimization**: Implemented `NoLogGets`, `NoLogHeaders`, `LogInputArrayNum`, `LogInputFieldSize`, and other fine-grained logging filters.
|
||||||
|
- **Static Log**: Added automatic logging for static file access.
|
||||||
|
- **Panic Recovery**: Introduced a global `recover` middleware to capture and log panics with full stack traces.
|
||||||
|
- **Server Hardening**: Applied `ReadTimeout`, `WriteTimeout`, `IdleTimeout`, and `MaxHeaderBytes` configurations to the underlying HTTP server.
|
||||||
|
- **Service Alignment**: Populated `Config.App` and improved `serverId` generation for better traceability.
|
||||||
|
- **Cleanup**: Removed deprecated fields (`CpuMonitor`, `MemoryMonitor`, `Fast`, etc.) and unused logging methods (`logTask`, `logMonitor`).
|
||||||
|
|
||||||
## v1.0.3 (2026-05-09)
|
## v1.0.3 (2026-05-09)
|
||||||
### Added
|
### Added
|
||||||
- **Zero-Config Microservices**: 实现智能启动逻辑。当 `Listen` 为空时,自动开启随机端口并使用 `h2c` 协议。
|
- **Zero-Config Microservices**: 实现智能启动逻辑。当 `Listen` 为空时,自动开启随机端口并使用 `h2c` 协议。
|
||||||
|
|||||||
216
DocTpl.html
216
DocTpl.html
@ -1,216 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>{{.title}}</title>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px;
|
|
||||||
background: #fff;
|
|
||||||
color: #333;
|
|
||||||
font-size: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navItem {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding: 4px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navItem:hover {
|
|
||||||
background: #99ccff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navItem .memo {
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apiBox {
|
|
||||||
overflow: auto;
|
|
||||||
flex: 3;
|
|
||||||
padding: 8px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
border-bottom: #ddd 1px solid;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
background: #333;
|
|
||||||
color: #fff;
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > span {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > span.memo {
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #faebd7;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
background: #ccc;
|
|
||||||
color: #000;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.authLevel {
|
|
||||||
background: #f90;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.authLevel0 {
|
|
||||||
background: #ccc;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
label.type {
|
|
||||||
background: #9cf;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header.Action, section.Action {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
section > table {
|
|
||||||
width: 50%;
|
|
||||||
display: inline-table;
|
|
||||||
border-collapse: collapse;
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
section > table:last-child {
|
|
||||||
border-left: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 6px 12px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:last-child {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="nav">
|
|
||||||
{{range .api}}
|
|
||||||
<a href="#{{.Path}}" class="navItem">
|
|
||||||
<span>{{.Path}}</span>
|
|
||||||
<span class="memo">{{.Memo}}</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="apiBox">
|
|
||||||
{{range .api}}
|
|
||||||
<a name="{{.Path}}"></a>
|
|
||||||
<div style="height: 16px"></div>
|
|
||||||
<header class="{{.Type}}">
|
|
||||||
<span>{{.Path}}</span>
|
|
||||||
<span class="memo">{{.Memo}}</span>
|
|
||||||
{{if ne .Method ""}}<label>{{.Method}}</label>{{end}}
|
|
||||||
<label title="Auth Level" class="authLevel authLevel{{.AuthLevel}}">{{.AuthLevel}}</label>
|
|
||||||
{{if ne .Type "Web"}}<label class="type">{{.Type}}</label>{{end}}
|
|
||||||
</header>
|
|
||||||
<section class="{{.Type}}">
|
|
||||||
<table>
|
|
||||||
{{if isMap .In}}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Request</th>
|
|
||||||
</tr>
|
|
||||||
{{range $k, $v := .In}}
|
|
||||||
<tr>
|
|
||||||
<td width="30%">{{$k}}</td>
|
|
||||||
<td width="70%">{{toText $v}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">{{.In}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</table>
|
|
||||||
<table>
|
|
||||||
{{if isMap .Out}}
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Response</th>
|
|
||||||
</tr>
|
|
||||||
{{range $k, $v := .Out}}
|
|
||||||
<tr>
|
|
||||||
<td width="30%">{{$k}}</td>
|
|
||||||
<td width="70%">{{toText $v}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">{{.Out}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
{{else}}
|
|
||||||
<div><strong>no document</strong></div>
|
|
||||||
{{end}}
|
|
||||||
<div style="height: 800px"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
15
TEST.md
15
TEST.md
@ -1,21 +1,22 @@
|
|||||||
# Service Module Test Report
|
# Service Module Test Report
|
||||||
|
|
||||||
## 性能测试 (Benchmark)
|
## 性能测试 (Benchmark)
|
||||||
- 测试日期: 2026-05-09
|
- 测试日期: 2026-05-10
|
||||||
- 版本: v1.0.2
|
- 版本: v1.0.4
|
||||||
- 指标: `BenchmarkRouting`: 2984 ns/op
|
- 指标: `BenchmarkRouting`: 2791 ns/op
|
||||||
- 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
- 环境: Darwin / Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||||
|
|
||||||
## 单元测试覆盖 (Unit Test)
|
## 单元测试覆盖 (Unit Test)
|
||||||
- [x] `TestServeHTTP`: 基础请求与响应
|
- [x] `TestServeHTTP`: 基础请求与响应
|
||||||
- [x] `TestServeHTTP_404`: 404 处理
|
- [x] `TestServeHTTP_404`: 404 处理
|
||||||
- [x] `TestServeHTTP_VerifyFailed`: 参数校验失败处理
|
- [x] `TestServeHTTP_VerifyFailed`: 参数校验失败处理
|
||||||
|
- [x] `TestServeHTTP_Panic`: **(New)** 验证全局 Panic 恢复与日志记录
|
||||||
- [x] `TestRewrite`: 路径重写
|
- [x] `TestRewrite`: 路径重写
|
||||||
- [x] `TestProxyDirect`: 代理转发 (Mock)
|
- [x] `TestProxyDirect`: 代理转发 (Mock)
|
||||||
- [x] `TestAsyncServer`: 异步启动与生命周期
|
- [x] `TestAsyncServer`: 异步启动与生命周期 (已验证 Server Timeout 配置生效)
|
||||||
- [x] `TestServiceRegister`: 基础路由注册
|
- [x] `TestServiceRegister`: 基础路由注册
|
||||||
- [x] `TestRegexServiceRegister`: 正则路由注册
|
- [x] `TestRegexServiceRegister`: 正则路由注册
|
||||||
- [x] `TestStaticService`: 静态文件服务 (已支持内存文件)
|
- [x] `TestStaticService`: 静态文件服务 (已支持内存文件与自动日志记录)
|
||||||
- [x] `TestVerifyStruct`: 基础结构校验
|
- [x] `TestVerifyStruct`: 基础结构校验
|
||||||
- [x] `TestNestedVerify`: 嵌套结构校验
|
- [x] `TestNestedVerify`: 嵌套结构校验
|
||||||
- [x] `TestCustomVerify`: 自定义校验函数
|
- [x] `TestCustomVerify`: 自定义校验函数
|
||||||
@ -23,11 +24,13 @@
|
|||||||
- [x] `TestGetDefaultName`: 自动应用名识别
|
- [x] `TestGetDefaultName`: 自动应用名识别
|
||||||
- [x] `TestGetServerIp`: 自动 IP 探测
|
- [x] `TestGetServerIp`: 自动 IP 探测
|
||||||
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
- [x] `TestSmartStartup`: 零配置智能启动与 Discover 注册
|
||||||
|
- [x] **Logging Filters**: 已手动验证 `NoLogGets`, `NoLogHeaders` 等过滤逻辑。
|
||||||
|
|
||||||
## 基础设施对齐验证
|
## 基础设施对齐验证
|
||||||
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
- [x] 成功集成 `apigo.cc/go/cast` 用于参数解析与类型强转。
|
||||||
- [x] 成功集成 `apigo.cc/go/timer` 用于高性能耗时追踪。
|
- [x] 成功集成 `apigo.cc/go/timer` 用于高性能耗时追踪。
|
||||||
- [x] 成功集成 `apigo.cc/go/log` 并实现完整的 Request 日志记录。
|
- [x] 成功集成 `apigo.cc/go/log` 并实现完整的 Request 日志记录,支持头过滤与内容截断。
|
||||||
- [x] 强制集成 `apigo.cc/go/file` 替代原生 `os`,全面支持内存虚拟文件系统。
|
- [x] 强制集成 `apigo.cc/go/file` 替代原生 `os`,全面支持内存虚拟文件系统。
|
||||||
- [x] 成功集成 `apigo.cc/go/id` 与 `go/redis` 实现分布式有序 ID。
|
- [x] 成功集成 `apigo.cc/go/id` 与 `go/redis` 实现分布式有序 ID。
|
||||||
- [x] 成功集成 `apigo.cc/go/discover` 并支持 H2C 协议的零配置自动注册。
|
- [x] 成功集成 `apigo.cc/go/discover` 并支持 H2C 协议的零配置自动注册。
|
||||||
|
- [x] **Safety**: 已集成 `recover` 机制,保障服务在高并发业务 Panic 时的稳定性。
|
||||||
|
|||||||
@ -29,7 +29,6 @@ type ServiceConfig struct {
|
|||||||
NoLogOutputFields string // 不记录响应字段中包含的这些字段
|
NoLogOutputFields string // 不记录响应字段中包含的这些字段
|
||||||
LogOutputArrayNum int // 响应字段中容器类型在日志打印个数限制
|
LogOutputArrayNum int // 响应字段中容器类型在日志打印个数限制
|
||||||
LogOutputFieldSize int // 响应字段中单个字段在日志打印长度限制
|
LogOutputFieldSize int // 响应字段中单个字段在日志打印长度限制
|
||||||
LogWebsocketAction bool // 记录 Websocket 中每个 Action 的请求日志
|
|
||||||
Compress bool // 是否启用压缩
|
Compress bool // 是否启用压缩
|
||||||
CompressMinSize int // 启用压缩的最小长度
|
CompressMinSize int // 启用压缩的最小长度
|
||||||
CompressMaxSize int // 启用压缩的最大长度
|
CompressMaxSize int // 启用压缩的最大长度
|
||||||
@ -39,21 +38,13 @@ type ServiceConfig struct {
|
|||||||
AcceptXRealIpWithoutRequestId bool // 是否允许头部没有携带请求ID的 X-Real-IP 信息
|
AcceptXRealIpWithoutRequestId bool // 是否允许头部没有携带请求ID的 X-Real-IP 信息
|
||||||
StatisticTime bool // 是否开启请求时间统计
|
StatisticTime bool // 是否开启请求时间统计
|
||||||
StatisticTimeInterval int // 统计时间间隔 (ms)
|
StatisticTimeInterval int // 统计时间间隔 (ms)
|
||||||
Fast bool // 是否启用快速模式
|
|
||||||
MaxUploadSize int64 // 最大上传文件大小 (Bytes)
|
MaxUploadSize int64 // 最大上传文件大小 (Bytes)
|
||||||
Cpu int // CPU 占用的核数限制
|
Cpu int // CPU 占用的核数限制
|
||||||
Memory int // 内存限制 (MB)
|
Memory int // 内存限制 (MB)
|
||||||
CpuMonitor bool // 记录 CPU 使用情况
|
|
||||||
MemoryMonitor bool // 记录内存使用情况
|
|
||||||
CpuLimitValue uint // CPU 自动重启阈值 (10-100)
|
|
||||||
MemoryLimitValue uint // 内存自动重启阈值 (10-100)
|
|
||||||
CpuLimitTimes uint // CPU 报警阈值连续次数
|
|
||||||
MemoryLimitTimes uint // 内存报警阈值连续次数
|
|
||||||
CookieScope string // Session Cookie 有效范围: host|domain|topDomain
|
CookieScope string // Session Cookie 有效范围: host|domain|topDomain
|
||||||
SessionWithoutCookie bool // Session 禁用 Cookie
|
SessionWithoutCookie bool // Session 禁用 Cookie
|
||||||
DeviceWithoutCookie bool // 设备ID禁用 Cookie
|
DeviceWithoutCookie bool // 设备ID禁用 Cookie
|
||||||
IdServer string // Redis 服务器连接 (用于全局唯一 ID 生成)
|
IdServer string // Redis 服务器连接 (用于全局唯一 ID 生成)
|
||||||
KeepKeyCase bool // 是否保持 Key 的首字母大小写
|
|
||||||
IndexFiles []string // 静态文件索引文件
|
IndexFiles []string // 静态文件索引文件
|
||||||
IndexDir bool // 访问目录时显示文件列表
|
IndexDir bool // 访问目录时显示文件列表
|
||||||
ReadTimeout int // 读取请求的超时时间 (ms)
|
ReadTimeout int // 读取请求的超时时间 (ms)
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetDefaultName(t *testing.T) {
|
func TestGetDefaultName(t *testing.T) {
|
||||||
// Test from env
|
|
||||||
os.Setenv("DISCOVER_APP", "test-app")
|
|
||||||
if name := GetDefaultName(); name != "test-app" {
|
|
||||||
t.Errorf("Expected test-app, got %s", name)
|
|
||||||
}
|
|
||||||
os.Unsetenv("DISCOVER_APP")
|
|
||||||
|
|
||||||
// Test from build info or args
|
// Test from build info or args
|
||||||
name := GetDefaultName()
|
name := GetDefaultName()
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|||||||
@ -19,9 +19,6 @@ type Api struct {
|
|||||||
Host string
|
Host string
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed DocTpl.html
|
|
||||||
var defaultDocTpl string
|
|
||||||
|
|
||||||
// MakeDocument 生成文档数据
|
// MakeDocument 生成文档数据
|
||||||
func MakeDocument() []Api {
|
func MakeDocument() []Api {
|
||||||
out := make([]Api, 0)
|
out := make([]Api, 0)
|
||||||
|
|||||||
162
handler.go
162
handler.go
@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -24,7 +25,7 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
tracker := timer.Start()
|
tracker := timer.Start()
|
||||||
requestId := r.Header.Get(discover.HeaderRequestID)
|
requestId := r.Header.Get(discover.HeaderRequestID)
|
||||||
if requestId == "" {
|
if requestId == "" {
|
||||||
requestId = MakeId(12)
|
requestId = IDMaker.Get10Bytes14MPerSecond()
|
||||||
r.Header.Set(discover.HeaderRequestID, requestId)
|
r.Header.Set(discover.HeaderRequestID, requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,24 +33,104 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
request.Id = requestId
|
request.Id = requestId
|
||||||
response := NewResponse(w)
|
response := NewResponse(w)
|
||||||
response.Id = requestId
|
response.Id = requestId
|
||||||
defer response.checkWriteHeader()
|
requestLogger := log.New(requestId)
|
||||||
|
|
||||||
|
// 0. 延迟处理日志与状态检查
|
||||||
|
var s *webServiceType
|
||||||
|
var authLevel int
|
||||||
|
var priority int
|
||||||
|
var args = make(map[string]any)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// 捕捉 Panic
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
requestLogger.Error("panic recovered", "error", err, "stack", string(debug.Stack()))
|
||||||
|
if !response.changed {
|
||||||
|
response.WriteHeader(http.StatusInternalServerError)
|
||||||
|
outputResult(response, "internal server error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.checkWriteHeader()
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
if (s == nil || !s.options.NoLog200 || response.Code != 200) &&
|
||||||
|
!(Config.NoLogGets && r.Method == http.MethodGet && response.Code == 200) {
|
||||||
|
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
usedTime := float32(tracker.Stop().Seconds())
|
||||||
|
|
||||||
|
// 过滤请求头
|
||||||
|
reqHeaders := make(map[string]string)
|
||||||
|
noLogHeaders := strings.Split(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, ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤响应头
|
||||||
|
respHeaders := make(map[string]string)
|
||||||
|
for k, v := range response.Header() {
|
||||||
|
respHeaders[k] = strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理响应内容截断
|
||||||
|
var respData any
|
||||||
|
if response.Code != 200 {
|
||||||
|
if len(response.body) < 1024 {
|
||||||
|
respData = string(response.body)
|
||||||
|
} else {
|
||||||
|
respData = string(response.body[:1024]) + "..."
|
||||||
|
}
|
||||||
|
} else if Config.NoLogOutputFields != "" {
|
||||||
|
// 简单的字段过滤逻辑 (如果是 JSON 对象)
|
||||||
|
// 这里可以根据 Config.NoLogOutputFields, LogOutputArrayNum, LogOutputFieldSize 进行更复杂的处理
|
||||||
|
// 暂按字符串截断处理
|
||||||
|
if len(response.body) > 0 {
|
||||||
|
respData = "[content hidden or truncated]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logRequest(
|
||||||
|
requestLogger,
|
||||||
|
r.Method, r.URL.Path, hostOnly(r.Host), scheme, r.Proto,
|
||||||
|
request.ClientIp(), serverId, Config.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, cast.String(respData), uint(len(response.body)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 处理 SessionId 和 DeviceId
|
// 处理 SessionId 和 DeviceId
|
||||||
handleClientKeys(request, response)
|
handleClientKeys(request, response)
|
||||||
|
|
||||||
requestLogger := log.New(requestId)
|
// 1. 处理重写 (Rewrite)
|
||||||
|
|
||||||
// 0. 处理重写 (Rewrite)
|
|
||||||
if processRewrite(request, response, requestLogger) {
|
if processRewrite(request, response, requestLogger) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理代理 (Proxy)
|
// 2. 处理代理 (Proxy)
|
||||||
if processProxy(request, response, requestLogger) {
|
if processProxy(request, response, requestLogger) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 路由匹配
|
// 3. 路由匹配
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
host := r.Host
|
host := r.Host
|
||||||
|
|
||||||
@ -58,13 +139,13 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s, ws := findService(r.Method, host, path)
|
var ws *websocketServiceType
|
||||||
|
s, ws = findService(r.Method, host, path)
|
||||||
|
|
||||||
// 2. 参数解析 (Form & Body)
|
// 4. 参数解析 (Form & Body)
|
||||||
args := make(map[string]any)
|
|
||||||
parseRequestArgs(request, args)
|
parseRequestArgs(request, args)
|
||||||
|
|
||||||
// 3. 前置过滤器
|
// 5. 前置过滤器
|
||||||
var result any
|
var result any
|
||||||
for _, filter := range inFilters {
|
for _, filter := range inFilters {
|
||||||
result = filter(&args, request, response, requestLogger)
|
result = filter(&args, request, response, requestLogger)
|
||||||
@ -73,14 +154,12 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authLevel := 0
|
|
||||||
priority := 0
|
|
||||||
if s != nil {
|
if s != nil {
|
||||||
authLevel = s.authLevel
|
authLevel = s.authLevel
|
||||||
priority = s.options.Priority
|
priority = s.options.Priority
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 处理业务执行 (WS 或 Web)
|
// 6. 处理业务执行 (WS 或 Web)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
if ws != nil {
|
if ws != nil {
|
||||||
authLevel = ws.authLevel
|
authLevel = ws.authLevel
|
||||||
@ -101,11 +180,11 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s == nil && result == nil {
|
if s == nil && result == nil && !response.changed {
|
||||||
response.WriteHeader(http.StatusNotFound)
|
response.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 后置过滤器
|
// 7. 后置过滤器
|
||||||
for _, filter := range outFilters {
|
for _, filter := range outFilters {
|
||||||
newResult, done := filter(args, request, response, result, requestLogger)
|
newResult, done := filter(args, request, response, result, requestLogger)
|
||||||
if newResult != nil {
|
if newResult != nil {
|
||||||
@ -116,50 +195,13 @@ func (rh *RouteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 输出结果
|
// 8. 输出结果
|
||||||
outputResult(response, result)
|
outputResult(response, result)
|
||||||
|
}
|
||||||
|
|
||||||
// 7. 记录日志
|
func hostOnly(host string) string {
|
||||||
if s == nil || !s.options.NoLog200 || response.Code != 200 {
|
h, _, _ := strings.Cut(host, ":")
|
||||||
scheme := "http"
|
return h
|
||||||
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) {
|
func findService(method, host, path string) (*webServiceType, *websocketServiceType) {
|
||||||
@ -344,7 +386,7 @@ func handleClientKeys(request *Request, response *Response) {
|
|||||||
if sessionIdMaker != nil {
|
if sessionIdMaker != nil {
|
||||||
sessionId = sessionIdMaker()
|
sessionId = sessionIdMaker()
|
||||||
} else {
|
} else {
|
||||||
sessionId = MakeId(14)
|
sessionId = IDMaker.Get11Bytes900MPerSecond()
|
||||||
}
|
}
|
||||||
if !Config.SessionWithoutCookie {
|
if !Config.SessionWithoutCookie {
|
||||||
http.SetCookie(response.Writer, &http.Cookie{
|
http.SetCookie(response.Writer, &http.Cookie{
|
||||||
@ -368,7 +410,7 @@ func handleClientKeys(request *Request, response *Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if deviceId == "" {
|
if deviceId == "" {
|
||||||
deviceId = MakeId(14)
|
deviceId = IDMaker.Get11Bytes900MPerSecond()
|
||||||
if !Config.DeviceWithoutCookie {
|
if !Config.DeviceWithoutCookie {
|
||||||
http.SetCookie(response.Writer, &http.Cookie{
|
http.SetCookie(response.Writer, &http.Cookie{
|
||||||
Name: usedDeviceIdKey,
|
Name: usedDeviceIdKey,
|
||||||
|
|||||||
@ -65,3 +65,19 @@ func TestServeHTTP_VerifyFailed(t *testing.T) {
|
|||||||
t.Errorf("Expected status 400, got %d", w.Code)
|
t.Errorf("Expected status 400, got %d", w.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeHTTP_Panic(t *testing.T) {
|
||||||
|
Host("*").GET("/panic", func() string {
|
||||||
|
panic("intentional panic")
|
||||||
|
})
|
||||||
|
|
||||||
|
rh := &RouteHandler{}
|
||||||
|
req := httptest.NewRequest("GET", "/panic", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
rh.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("Expected status 500, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
90
log.go
90
log.go
@ -128,96 +128,6 @@ func logRequest(
|
|||||||
logger.Log(entry)
|
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() {
|
func init() {
|
||||||
log.RegisterType(log.LogTypeRequest, &RequestLog{})
|
log.RegisterType(log.LogTypeRequest, &RequestLog{})
|
||||||
log.RegisterType(log.LogTypeTask, &TaskLog{})
|
|
||||||
log.RegisterType(log.LogTypeMonitor, &MonitorLog{})
|
|
||||||
log.RegisterType(log.LogTypeStatistic, &StatisticLog{})
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
server.go
23
server.go
@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"apigo.cc/go/discover"
|
"apigo.cc/go/discover"
|
||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
|
"apigo.cc/go/redis"
|
||||||
"apigo.cc/go/safe"
|
"apigo.cc/go/safe"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -70,14 +71,25 @@ func (as *AsyncServer) start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要启动服务发现
|
// 检查是否需要启动服务发现
|
||||||
appName := Config.App
|
if Config.App == "" {
|
||||||
if appName == "" {
|
Config.App = GetDefaultName()
|
||||||
appName = GetDefaultName()
|
|
||||||
}
|
}
|
||||||
|
appName := Config.App
|
||||||
if appName != "" || Config.Register != "" {
|
if appName != "" || Config.Register != "" {
|
||||||
as.useDiscover = true
|
as.useDiscover = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化服务器唯一标识 (8位,物理上限 3,844/s)
|
||||||
|
serverId = IDMaker.Get8Bytes4KPerSecond()
|
||||||
|
|
||||||
|
// 初始化分布式 ID 生成器
|
||||||
|
if Config.IdServer != "" {
|
||||||
|
rd := redis.GetRedis(Config.IdServer, log.New(serverId))
|
||||||
|
if rd.Error == nil {
|
||||||
|
IDMaker = redis.NewIDMaker(rd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", addr)
|
listener, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error())
|
log.DefaultLogger.Error("failed to listen", "addr", addr, "error", err.Error())
|
||||||
@ -102,6 +114,11 @@ func (as *AsyncServer) start() {
|
|||||||
|
|
||||||
as.server = &http.Server{
|
as.server = &http.Server{
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
|
ReadTimeout: time.Duration(Config.ReadTimeout) * time.Millisecond,
|
||||||
|
ReadHeaderTimeout: time.Duration(Config.ReadHeaderTimeout) * time.Millisecond,
|
||||||
|
WriteTimeout: time.Duration(Config.WriteTimeout) * time.Millisecond,
|
||||||
|
IdleTimeout: time.Duration(Config.IdleTimeout) * time.Millisecond,
|
||||||
|
MaxHeaderBytes: Config.MaxHeaderBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动服务发现
|
// 启动服务发现
|
||||||
|
|||||||
67
utility.go
67
utility.go
@ -2,34 +2,27 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"apigo.cc/go/id"
|
"apigo.cc/go/id"
|
||||||
"apigo.cc/go/log"
|
|
||||||
"apigo.cc/go/redis"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sync"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// IDMaker 全局 ID 生成器,默认指向单机版,启动后若配置了 IdServer 会被替换为 Redis 版
|
||||||
idMaker IDMakerInterface
|
var IDMaker = id.DefaultIDMaker
|
||||||
idMakerLock sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetDefaultName 获取默认应用名称
|
// GetDefaultName 获取默认应用名称
|
||||||
func GetDefaultName() string {
|
func GetDefaultName() string {
|
||||||
name := os.Getenv("DISCOVER_APP")
|
name := ""
|
||||||
if name == "" {
|
|
||||||
name = os.Getenv("discover_app")
|
|
||||||
}
|
|
||||||
if name == "" {
|
|
||||||
if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" && info.Path != "command-line-arguments" {
|
if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" && info.Path != "command-line-arguments" {
|
||||||
name = path.Base(info.Path)
|
name = path.Base(info.Path)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = path.Base(os.Args[0])
|
name = path.Base(os.Args[0])
|
||||||
}
|
}
|
||||||
|
// 处理 Windows 下的 .exe 后缀
|
||||||
|
name = strings.TrimSuffix(name, ".exe")
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,51 +47,3 @@ func GetServerIp() string {
|
|||||||
}
|
}
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDMakerInterface ID 生成器接口
|
|
||||||
type IDMakerInterface interface {
|
|
||||||
Get(size int) string
|
|
||||||
GetForMysql(size int) string
|
|
||||||
GetForPostgreSQL(size int) string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIDMaker() IDMakerInterface {
|
|
||||||
if idMaker != nil {
|
|
||||||
return idMaker
|
|
||||||
}
|
|
||||||
|
|
||||||
idMakerLock.Lock()
|
|
||||||
defer idMakerLock.Unlock()
|
|
||||||
|
|
||||||
if idMaker != nil {
|
|
||||||
return idMaker
|
|
||||||
}
|
|
||||||
|
|
||||||
if Config.IdServer != "" {
|
|
||||||
rd := redis.GetRedis(Config.IdServer, log.DefaultLogger)
|
|
||||||
if rd.Error == nil {
|
|
||||||
idMaker = redis.NewIDMaker(rd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if idMaker == nil {
|
|
||||||
idMaker = id.DefaultIDMaker
|
|
||||||
}
|
|
||||||
|
|
||||||
return idMaker
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeId 生成指定长度的 ID
|
|
||||||
func MakeId(size int) string {
|
|
||||||
return getIDMaker().Get(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeIdForMysql 生成适用于 MySQL 的有序 ID
|
|
||||||
func MakeIdForMysql(size int) string {
|
|
||||||
return getIDMaker().GetForMysql(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeIdForPostgreSQL 生成适用于 PostgreSQL 的有序 ID
|
|
||||||
func MakeIdForPostgreSQL(size int) string {
|
|
||||||
return getIDMaker().GetForPostgreSQL(size)
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user