请求日志: 显示响应体 + 新增 ShowFullUrl/ShowRunTime 配置(默认true)
This commit is contained in:
@@ -16,14 +16,16 @@ import simapi.configurations.*
|
||||
import simapi.helpers.*
|
||||
|
||||
/**
|
||||
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、耗时与异常。
|
||||
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、响应体、耗时与异常。
|
||||
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
|
||||
*
|
||||
* 对齐说明:
|
||||
* - 请求体按 JSON 字段级截断(对齐 C#:仅对超长字符串字段截断,保留结构)
|
||||
* - 捕获下游异常并记录,随后重抛(对齐 C# ExceptionDispatchInfo + edi.Throw)
|
||||
* - 响应体:soulsoft HttpResponse.body 只读不可替换(C# 用 MemoryStream 替换捕获),
|
||||
* 此处以 Content-Length 作为替代信息;ShowFullResponse 选项因此暂不生效
|
||||
* - 响应体:soulsoft HttpResponse.body 只读且不可读回(read 抛 UnsupportedException),
|
||||
* 无法像 C# 那样用 MemoryStream 替换捕获;改为在各统一写出入口
|
||||
* (SimApiResponseWriter)缓存响应文本,此处直接读取。ShowFullResponse=false 时截断到 200 字符(对齐 C#)
|
||||
* - ShowFullUrl=false 时仅显示路径+查询串;ShowRunTime=true 时请求行显示 [POST] (xxxms)
|
||||
*/
|
||||
public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
private let _options: SimApiOptions
|
||||
@@ -39,13 +41,11 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
*/
|
||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||
let start = MonoTime.now()
|
||||
let fullUrl = context.request.getDisplayUrl()
|
||||
let options = _options.simApiRequestLogOptions
|
||||
var sb = StringBuilder()
|
||||
|
||||
sb.append("[${context.request.method}] ${fullUrl}\n")
|
||||
|
||||
// 请求头
|
||||
if (_options.simApiRequestLogOptions.showFullHeader) {
|
||||
if (options.showFullHeader) {
|
||||
sb.append("*( RequestHeaders [Full] ) =>\n")
|
||||
sb.append(serializeHeaders(context))
|
||||
} else {
|
||||
@@ -67,20 +67,30 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
exception = Some(ex)
|
||||
}
|
||||
|
||||
// 响应信息
|
||||
// 请求行(耗时需在 next 之后计算,故最后拼装)
|
||||
let elapsed = MonoTime.now() - start
|
||||
let elapsedMs = elapsed / Duration.millisecond
|
||||
sb.append("*( Response [${context.response.statusCode}] ) => ${elapsedMs}ms")
|
||||
// 响应体捕获受 soulsoft 限制(body 只读不可替换),记录 Content-Length 作为替代
|
||||
if (let Some(len) <- context.response.contentLength) {
|
||||
sb.append(" (响应体长度: ${len})")
|
||||
let url = if (options.showFullUrl) {
|
||||
buildDisplayUrl(context)
|
||||
} else {
|
||||
context.request.path.toString() + context.request.queryString.toString()
|
||||
}
|
||||
var sbHead = StringBuilder()
|
||||
sbHead.append("[${context.request.method}]")
|
||||
if (options.showRunTime) {
|
||||
sbHead.append(" (${elapsedMs}ms)")
|
||||
}
|
||||
sbHead.append(" ${url}\n")
|
||||
|
||||
// 响应信息:状态码 + 响应体(经 SimApiResponseWriter 缓存读取)
|
||||
sb.append("*( Response [${context.response.statusCode}] ) =>")
|
||||
sb.append(readResponseBody(context))
|
||||
sb.append("\n")
|
||||
if (let Some(ex) <- exception) {
|
||||
sb.append("Exception: ${ex.toString()}\n")
|
||||
}
|
||||
|
||||
_logger.info(sb.toString())
|
||||
_logger.info(sbHead.toString() + sb.toString())
|
||||
|
||||
// 重抛原异常(对齐 C# edi?.Throw()),由外层 ExceptionMiddleware 处理
|
||||
if (let Some(ex) <- exception) {
|
||||
@@ -88,6 +98,27 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造完整请求 URL:stdx 服务端请求 URL 只有路径(无 scheme/host),需手动拼接。
|
||||
/// 对齐 C# 的 {Scheme}://{Host}{Path}{QueryString};Host 优先取 Host 请求头。
|
||||
private func buildDisplayUrl(context: HttpContext): String {
|
||||
var sb = StringBuilder()
|
||||
let scheme = context.request.scheme
|
||||
sb.append(if (scheme.isEmpty()) { "http" } else { scheme })
|
||||
sb.append("://")
|
||||
let hostHeader = context.request.headers.get("Host") ?? ""
|
||||
if (!hostHeader.isEmpty()) {
|
||||
sb.append(hostHeader)
|
||||
} else {
|
||||
let host = context.request.host.toString()
|
||||
if (!host.isEmpty()) {
|
||||
sb.append(host)
|
||||
}
|
||||
}
|
||||
sb.append(context.request.path.toString())
|
||||
sb.append(context.request.queryString.toString())
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
private func serializeHeaders(context: HttpContext): String {
|
||||
var sb = StringBuilder()
|
||||
sb.append("{")
|
||||
@@ -134,6 +165,29 @@ public class SimApiRequestLogMiddleware <: IMiddleware {
|
||||
}
|
||||
}
|
||||
|
||||
/// 响应体读取:从 SimApiResponseWriter 缓存取;未捕获到时回退 Content-Length
|
||||
private func readResponseBody(context: HttpContext): String {
|
||||
let body = match (context.items.get(SimApiResponseWriter.responseBodyCacheKey)) {
|
||||
case Some(v) => if (let s: String <- v) { s } else { "" }
|
||||
case None => ""
|
||||
}
|
||||
if (body.isEmpty()) {
|
||||
// 未捕获到响应体(如 IActionResult 直写路径):记录 Content-Length 作为替代
|
||||
if (let Some(len) <- context.response.contentLength) {
|
||||
return " (响应体长度: ${len})"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if (_options.simApiRequestLogOptions.showFullResponse) {
|
||||
return body
|
||||
}
|
||||
// 对齐 C#:ShowFullResponse=false 时截断到 200 字符
|
||||
if (body.size > 200) {
|
||||
return body[0..200] + "...(${body.size})"
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
/// 请求体截断:JSON 字段级截断(对齐 C#:仅对超长字符串字段截断),非 JSON 则整串截断
|
||||
private func truncateBody(body: String): String {
|
||||
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
|
||||
|
||||
Reference in New Issue
Block a user