feat: RequestLog 中间件补齐(对齐 C#)

- 请求体 JSON 字段级截断(仅超长字符串字段,保留结构)
- 下游异常捕获记录后重抛(对齐 C# ExceptionDispatchInfo)
- 响应体受 soulsoft body 只读限制,记录 Content-Length 替代
This commit is contained in:
2026-08-16 22:46:57 +08:00
parent a04ab2f400
commit a73a986f8b
+59 -12
View File
@@ -8,14 +8,21 @@ package simapi.middlewares
import std.collection.*
import std.io.*
import std.time.*
import stdx.encoding.json.*
import soulsoft_web_http.*
import soulsoft_extensions_logging.*
import simapi.communications.*
import simapi.configurations.*
/**
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码耗时。
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码耗时与异常
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
*
* 对齐说明:
* - 请求体按 JSON 字段级截断(对齐 C#:仅对超长字符串字段截断,保留结构)
* - 捕获下游异常并记录,随后重抛(对齐 C# ExceptionDispatchInfo + edi.Throw
* - 响应体:soulsoft HttpResponse.body 只读不可替换(C# 用 MemoryStream 替换捕获),
* 此处以 Content-Length 作为替代信息;ShowFullResponse 选项因此暂不生效
*/
public class SimApiRequestLogMiddleware <: IMiddleware {
private let _options: SimApiOptions
@@ -51,15 +58,33 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
sb.append("*( RequestBody ) =>\n")
sb.append(readRequestBody(context))
// 调用下一级
next(context)
// 调用下一级,捕获异常以便记录并重抛(对齐 C# ExceptionDispatchInfo
var exception: ?Exception = None
try {
next(context)
} catch (ex: Exception) {
exception = Some(ex)
}
// 响应信息
let elapsed = MonoTime.now() - start
let elapsedMs = elapsed / Duration.millisecond
sb.append("*( Response [${context.response.statusCode}] ) => ${elapsedMs}ms\n")
sb.append("*( Response [${context.response.statusCode}] ) => ${elapsedMs}ms")
// 响应体捕获受 soulsoft 限制(body 只读不可替换),记录 Content-Length 作为替代
if (let Some(len) <- context.response.contentLength) {
sb.append(" (响应体长度: ${len})")
}
sb.append("\n")
if (let Some(ex) <- exception) {
sb.append("Exception: ${ex.toString()}\n")
}
_logger.info(sb.toString())
// 重抛原异常(对齐 C# edi?.Throw()),由外层 ExceptionMiddleware 处理
if (let Some(ex) <- exception) {
throw ex
}
}
private func serializeHeaders(context: HttpContext): String {
@@ -107,17 +132,39 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
}
}
/// 请求体截断:JSON 字段级截断(对齐 C#:仅对超长字符串字段截断),非 JSON 则整串截断
private func truncateBody(body: String): String {
if (_options.simApiRequestLogOptions.requestStringLogLength <= 0 ||
body.size <= _options.simApiRequestLogOptions.requestStringLogLength) {
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
if (maxLen <= 0) {
return body + "\n"
}
// 简单按长度截断(不做 JSON 字段级截断,保持实现简洁)
let chars = body.toArray()
var sb = StringBuilder()
for (i in 0.._options.simApiRequestLogOptions.requestStringLogLength) {
sb.append(chars[i])
try {
let jv = JsonValue.fromStr(body)
match (jv.kind()) {
case JsObject =>
let obj = jv.asObject()
let newObj = JsonObject()
for ((k, v) in obj.getFields()) {
match (v.kind()) {
case JsString =>
let str = v.asString().getValue()
if (str.size > maxLen) {
newObj.put(k, JsonString(str[0..maxLen] + "...(${str.size})"))
} else {
newObj.put(k, v)
}
case _ => newObj.put(k, v)
}
}
return newObj.toString() + "\n"
case _ => ()
}
} catch (_: Exception) {
}
return "${sb.toString()}...(${body.size})\n"
// 非 JSON 或解析失败:整串按长度截断
if (body.size <= maxLen) {
return body + "\n"
}
return body[0..maxLen] + "...(${body.size})\n"
}
}