2026-08-16 12:46:15 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
|
|
|
|
|
* 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
package simapi.middlewares
|
|
|
|
|
|
|
|
|
|
|
|
import std.collection.*
|
|
|
|
|
|
import std.io.*
|
|
|
|
|
|
import std.time.*
|
|
|
|
|
|
import soulsoft_web_http.*
|
|
|
|
|
|
import soulsoft_extensions_logging.*
|
2026-08-18 00:07:00 +08:00
|
|
|
|
import simapi_serialization.*
|
2026-08-16 12:46:15 +08:00
|
|
|
|
import simapi.communications.*
|
|
|
|
|
|
import simapi.configurations.*
|
2026-08-18 01:15:15 +08:00
|
|
|
|
import simapi.helpers.*
|
2026-08-16 12:46:15 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-16 22:46:57 +08:00
|
|
|
|
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、耗时与异常。
|
2026-08-16 12:46:15 +08:00
|
|
|
|
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
|
2026-08-16 22:46:57 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 对齐说明:
|
|
|
|
|
|
* - 请求体按 JSON 字段级截断(对齐 C#:仅对超长字符串字段截断,保留结构)
|
|
|
|
|
|
* - 捕获下游异常并记录,随后重抛(对齐 C# ExceptionDispatchInfo + edi.Throw)
|
|
|
|
|
|
* - 响应体:soulsoft HttpResponse.body 只读不可替换(C# 用 MemoryStream 替换捕获),
|
|
|
|
|
|
* 此处以 Content-Length 作为替代信息;ShowFullResponse 选项因此暂不生效
|
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 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))
|
|
|
|
|
|
|
2026-08-16 22:46:57 +08:00
|
|
|
|
// 调用下一级,捕获异常以便记录并重抛(对齐 C# ExceptionDispatchInfo)
|
|
|
|
|
|
var exception: ?Exception = None
|
|
|
|
|
|
try {
|
|
|
|
|
|
next(context)
|
|
|
|
|
|
} catch (ex: Exception) {
|
|
|
|
|
|
exception = Some(ex)
|
|
|
|
|
|
}
|
2026-08-16 12:46:15 +08:00
|
|
|
|
|
|
|
|
|
|
// 响应信息
|
|
|
|
|
|
let elapsed = MonoTime.now() - start
|
|
|
|
|
|
let elapsedMs = elapsed / Duration.millisecond
|
2026-08-16 22:46:57 +08:00
|
|
|
|
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")
|
|
|
|
|
|
}
|
2026-08-16 12:46:15 +08:00
|
|
|
|
|
|
|
|
|
|
_logger.info(sb.toString())
|
2026-08-16 22:46:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 重抛原异常(对齐 C# edi?.Throw()),由外层 ExceptionMiddleware 处理
|
|
|
|
|
|
if (let Some(ex) <- exception) {
|
|
|
|
|
|
throw ex
|
|
|
|
|
|
}
|
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(",") }
|
2026-08-18 01:15:15 +08:00
|
|
|
|
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()
|
2026-08-18 00:27:32 +08:00
|
|
|
|
// 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读)
|
2026-08-16 12:46:15 +08:00
|
|
|
|
if (let seekable: Seekable <- context.request.body) {
|
|
|
|
|
|
seekable.seek(SeekPosition.Begin(0))
|
|
|
|
|
|
}
|
2026-08-18 00:27:32 +08:00
|
|
|
|
context.items["SimApi:BodyCache"] = bodyText
|
2026-08-16 12:46:15 +08:00
|
|
|
|
return truncateBody(bodyText)
|
|
|
|
|
|
} catch (_: Exception) {
|
|
|
|
|
|
return "(读取请求体失败)\n"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 22:46:57 +08:00
|
|
|
|
/// 请求体截断:JSON 字段级截断(对齐 C#:仅对超长字符串字段截断),非 JSON 则整串截断
|
2026-08-16 12:46:15 +08:00
|
|
|
|
private func truncateBody(body: String): String {
|
2026-08-16 22:46:57 +08:00
|
|
|
|
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
|
|
|
|
|
|
if (maxLen <= 0) {
|
2026-08-16 12:46:15 +08:00
|
|
|
|
return body + "\n"
|
|
|
|
|
|
}
|
2026-08-16 22:46:57 +08:00
|
|
|
|
try {
|
2026-08-18 00:07:00 +08:00
|
|
|
|
// 用 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(",") }
|
2026-08-18 01:15:15 +08:00
|
|
|
|
sb.append("\"${SimApiUtil.escapeJson(k)}\":")
|
2026-08-18 00:07:00 +08:00
|
|
|
|
if (let s: String <- v) {
|
|
|
|
|
|
if (s.size > maxLen) {
|
2026-08-18 01:15:15 +08:00
|
|
|
|
sb.append("\"${SimApiUtil.escapeJson(s[0..maxLen])}...(${s.size})\"")
|
2026-08-18 00:07:00 +08:00
|
|
|
|
} else {
|
2026-08-18 01:15:15 +08:00
|
|
|
|
sb.append("\"${SimApiUtil.escapeJson(s)}\"")
|
2026-08-16 22:46:57 +08:00
|
|
|
|
}
|
2026-08-18 00:07:00 +08:00
|
|
|
|
} else {
|
2026-08-18 01:15:15 +08:00
|
|
|
|
sb.append(SimApiUtil.json(Some(v)))
|
2026-08-18 00:07:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
first = false
|
2026-08-16 22:46:57 +08:00
|
|
|
|
}
|
2026-08-18 00:07:00 +08:00
|
|
|
|
sb.append("}")
|
|
|
|
|
|
return sb.toString() + "\n"
|
2026-08-16 22:46:57 +08:00
|
|
|
|
} catch (_: Exception) {
|
2026-08-16 12:46:15 +08:00
|
|
|
|
}
|
2026-08-16 22:46:57 +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
|
|
|
|
}
|
|
|
|
|
|
}
|