Files
simapi-cj/src/middlewares/SimApiRequestLogMiddleware.cj
T
xrain a73a986f8b feat: RequestLog 中间件补齐(对齐 C#)
- 请求体 JSON 字段级截断(仅超长字符串字段,保留结构)
- 下游异常捕获记录后重抛(对齐 C# ExceptionDispatchInfo)
- 响应体受 soulsoft body 只读限制,记录 Content-Length 替代
2026-08-16 22:46:57 +08:00

171 lines
6.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 stdx.encoding.json.*
import soulsoft_web_http.*
import soulsoft_extensions_logging.*
import simapi.communications.*
import simapi.configurations.*
/**
* 请求日志中间件:记录请求方法、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
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 fullUrl = context.request.getDisplayUrl()
var sb = StringBuilder()
sb.append("[${context.request.method}] ${fullUrl}\n")
// 请求头
if (_options.simApiRequestLogOptions.showFullHeader) {
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)
}
// 响应信息
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})")
}
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 {
var sb = StringBuilder()
sb.append("{")
var first = true
for ((name, values) in context.request.headers) {
if (!first) { sb.append(",") }
sb.append("\"${SimApiJson.escapeJson(name)}\":\"${SimApiJson.escapeJson(joinValues(values))}\"")
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()
// 重置流位置,供后续业务读取
if (let seekable: Seekable <- context.request.body) {
seekable.seek(SeekPosition.Begin(0))
}
return truncateBody(bodyText)
} catch (_: Exception) {
return "(读取请求体失败)\n"
}
}
/// 请求体截断:JSON 字段级截断(对齐 C#:仅对超长字符串字段截断),非 JSON 则整串截断
private func truncateBody(body: String): String {
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
if (maxLen <= 0) {
return body + "\n"
}
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) {
}
// 非 JSON 或解析失败:整串按长度截断
if (body.size <= maxLen) {
return body + "\n"
}
return body[0..maxLen] + "...(${body.size})\n"
}
}