Files
simapi-cj/src/middlewares/SimApiRequestLogMiddleware.cj
T

228 lines
8.6 KiB
Plaintext
Raw Normal View History

2026-08-16 12:46:15 +08:00
/*
* Copyright (c) 2025 SimcuTeam. All rights reserved.
* 移植自 C# 项目 SimApiE:\simcu\simapi-net),遵循 MIT 许可证。
*/
package simapi.middlewares
import std.collection.*
import std.io.*
import std.time.*
import soulsoft_web_http.*
import soulsoft_extensions_logging.*
import simapi_serialization.*
2026-08-16 12:46:15 +08:00
import simapi.communications.*
import simapi.configurations.*
import simapi.helpers.*
2026-08-16 12:46:15 +08:00
/**
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、响应体、耗时与异常。
2026-08-16 12:46:15 +08:00
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
*
* 对齐说明:
* - 请求体按 JSON 字段级截断(对齐 C#:仅对超长字符串字段截断,保留结构)
* - 捕获下游异常并记录,随后重抛(对齐 C# ExceptionDispatchInfo + edi.Throw
* - 响应体:soulsoft HttpResponse.body 只读且不可读回(read 抛 UnsupportedException),
* 无法像 C# 那样用 MemoryStream 替换捕获;改为在各统一写出入口
* SimApiResponseWriter)缓存响应文本,此处直接读取。ShowFullResponse=false 时截断到 200 字符(对齐 C#)
* - ShowFullUrl=false 时仅显示路径+查询串;ShowRunTime=true 时请求行显示 [POST] (xxxms)
2026-08-16 12:46:15 +08:00
*/
public class SimApiRequestLogMiddleware <: IMiddleware {
private let _options: SimApiOptions
private let _logger: ILogger
public init(options: SimApiOptions, loggerFactory: ILoggerFactory) {
_options = options
_logger = loggerFactory.createLogger("SimApi.RequestLog")
}
/**
* 中间件入口(IMiddleware 风格,对齐 C# UseMiddleware<T> 的 InvokeAsync)。
*/
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
let start = MonoTime.now()
let options = _options.simApiRequestLogOptions
2026-08-16 12:46:15 +08:00
var sb = StringBuilder()
// 请求头
if (options.showFullHeader) {
2026-08-16 12:46:15 +08:00
sb.append("*( RequestHeaders [Full] ) =>\n")
sb.append(serializeHeaders(context))
} else {
sb.append("*( RequestHeaders ) =>\n")
let token = context.request.headers.get("Token") ?? ""
let queryId = context.request.headers.get("Query-Id") ?? ""
sb.append("Token: ${token} QueryId: ${queryId}\n")
}
// 请求体
sb.append("*( RequestBody ) =>\n")
sb.append(readRequestBody(context))
// 调用下一级,捕获异常以便记录并重抛(对齐 C# ExceptionDispatchInfo
var exception: ?Exception = None
try {
next(context)
} catch (ex: Exception) {
exception = Some(ex)
}
2026-08-16 12:46:15 +08:00
// 请求行(耗时需在 next 之后计算,故最后拼装)
2026-08-16 12:46:15 +08:00
let elapsed = MonoTime.now() - start
let elapsedMs = elapsed / Duration.millisecond
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")
}
2026-08-16 12:46:15 +08:00
_logger.info(sbHead.toString() + sb.toString())
// 重抛原异常(对齐 C# edi?.Throw()),由外层 ExceptionMiddleware 处理
if (let Some(ex) <- exception) {
throw ex
}
2026-08-16 12:46:15 +08:00
}
/// 构造完整请求 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()
}
2026-08-16 12:46:15 +08:00
private func serializeHeaders(context: HttpContext): String {
var sb = StringBuilder()
sb.append("{")
var first = true
for ((name, values) in context.request.headers) {
if (!first) { sb.append(",") }
sb.append("\"${SimApiUtil.escapeJson(name)}\":\"${SimApiUtil.escapeJson(joinValues(values))}\"")
2026-08-16 12:46:15 +08:00
first = false
}
sb.append("}\n")
sb.toString()
}
private func joinValues(values: Collection<String>): String {
var sb = StringBuilder()
var first = true
for (v in values) {
if (!first) { sb.append(",") }
sb.append(v)
first = false
}
sb.toString()
}
private func readRequestBody(context: HttpContext): String {
try {
context.request.enableBuffering()
var buffer = Array<Byte>(4096, repeat: 0)
var sb = StringBuilder()
var read = context.request.body.read(buffer)
while (read > 0) {
sb.appendFromUtf8(buffer.slice(0, read))
read = context.request.body.read(buffer)
}
let bodyText = sb.toString()
// 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读)
2026-08-16 12:46:15 +08:00
if (let seekable: Seekable <- context.request.body) {
seekable.seek(SeekPosition.Begin(0))
}
context.items["SimApi:BodyCache"] = bodyText
2026-08-16 12:46:15 +08:00
return truncateBody(bodyText)
} catch (_: Exception) {
return "(读取请求体失败)\n"
}
}
/// 响应体读取:从 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 则整串截断
2026-08-16 12:46:15 +08:00
private func truncateBody(body: String): String {
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
if (maxLen <= 0) {
2026-08-16 12:46:15 +08:00
return body + "\n"
}
try {
// 用 simapi_serialization 解析为动态结构,仅截断超长字符串字段(保持结构)
let map = JsonSerializer.Deserialize<HashMap<String, Any>>(body)
var sb = StringBuilder()
sb.append("{")
var first = true
for ((k, v) in map) {
if (!first) { sb.append(",") }
sb.append("\"${SimApiUtil.escapeJson(k)}\":")
if (let s: String <- v) {
if (s.size > maxLen) {
sb.append("\"${SimApiUtil.escapeJson(s[0..maxLen])}...(${s.size})\"")
} else {
sb.append("\"${SimApiUtil.escapeJson(s)}\"")
}
} else {
sb.append(SimApiUtil.json(Some(v)))
}
first = false
}
sb.append("}")
return sb.toString() + "\n"
} catch (_: Exception) {
2026-08-16 12:46:15 +08:00
}
// 非 JSON 或解析失败:整串按长度截断
if (body.size <= maxLen) {
return body + "\n"
}
return body[0..maxLen] + "...(${body.size})\n"
2026-08-16 12:46:15 +08:00
}
}