重构: 源码文件蛇形命名, 接口去I头(BindRequestContext/SimApiAuthChecker), 清理C#相关说明, serialization依赖路径改 serialization-cj
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimcuTeam. All rights reserved.
|
||||
* 遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simcu::simapi.middlewares
|
||||
|
||||
import std.collection.*
|
||||
import std.io.*
|
||||
import std.time.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_extensions_logging.*
|
||||
import simcu::serialization.*
|
||||
import simcu::simapi.communications.*
|
||||
import simcu::simapi.configurations.*
|
||||
import simcu::simapi.helpers.*
|
||||
|
||||
/**
|
||||
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码、响应体、耗时与异常。
|
||||
*
|
||||
* 对齐说明:
|
||||
* - 请求体按 JSON 字段级截断
|
||||
* - 捕获下游异常并记录,随后重抛
|
||||
* - 响应体:soulsoft HttpResponse.body 只读且不可读回(read 抛 UnsupportedException),
|
||||
* 无法像原版那样用 MemoryStream 替换捕获;改为在各统一写出入口
|
||||
* (SimApiResponseWriter)缓存响应文本,此处直接读取。
|
||||
* 响应行格式:*( Response [status] ) => [ N bytes ],随后换行输出响应体结构;
|
||||
* ShowFullResponse=false 时截断到 200 字符
|
||||
* - ShowFullUrl=false 时仅显示路径+查询串;ShowRunTime=true 时请求行显示 [POST] (xxxms)
|
||||
*/
|
||||
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 风格)。
|
||||
*/
|
||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||
let start = MonoTime.now()
|
||||
let options = _options.simApiRequestLogOptions
|
||||
var sb = StringBuilder()
|
||||
|
||||
// 请求头
|
||||
if (options.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))
|
||||
|
||||
// 调用下一级,捕获异常以便记录并重抛
|
||||
var exception: ?Exception = None
|
||||
try {
|
||||
next(context)
|
||||
} catch (ex: Exception) {
|
||||
exception = Some(ex)
|
||||
}
|
||||
|
||||
// 请求行(耗时需在 next 之后计算,故最后拼装)
|
||||
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")
|
||||
|
||||
// 响应信息:状态码 + 响应体长度(bytes),响应体结构换行显示
|
||||
let responseBody = readResponseBody(context)
|
||||
let bodyLen = if (responseBody.isEmpty()) {
|
||||
context.response.contentLength ?? 0
|
||||
} else {
|
||||
responseBody.size
|
||||
}
|
||||
sb.append("*( Response [${context.response.statusCode}] ) => [ ${bodyLen} bytes ]\n")
|
||||
if (!responseBody.isEmpty()) {
|
||||
let display = if (_options.simApiRequestLogOptions.showFullResponse) {
|
||||
responseBody
|
||||
} else if (responseBody.size > 200) {
|
||||
// ShowFullResponse=false 时截断到 200 字符(长度仍显示完整字节数)
|
||||
responseBody[0..200] + "...(${responseBody.size})"
|
||||
} else {
|
||||
responseBody
|
||||
}
|
||||
// 去掉响应体末尾换行:结构体最后不多加换行(logger 收尾会补一个换行)
|
||||
var text = display
|
||||
while (text.endsWith("\n")) {
|
||||
text = text[0..text.size - 1]
|
||||
}
|
||||
sb.append(text)
|
||||
}
|
||||
if (let Some(ex) <- exception) {
|
||||
sb.append("\nException: ${ex.toString()}\n")
|
||||
}
|
||||
|
||||
_logger.info(sbHead.toString() + sb.toString())
|
||||
|
||||
// 重抛原异常,由外层 ExceptionMiddleware 处理
|
||||
if (let Some(ex) <- exception) {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造完整请求 URL:stdx 服务端请求 URL 只有路径(无 scheme/host),需手动拼接。
|
||||
/// {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("{")
|
||||
var first = true
|
||||
for ((name, values) in context.request.headers) {
|
||||
if (!first) { sb.append(",") }
|
||||
sb.append("\"${SimApiUtil.escapeJson(name)}\":\"${SimApiUtil.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()
|
||||
// 重置流位置,供后续业务读取;同时缓存 body(流可能不可重读)
|
||||
if (let seekable: Seekable <- context.request.body) {
|
||||
seekable.seek(SeekPosition.Begin(0))
|
||||
}
|
||||
context.items["SimApi:BodyCache"] = bodyText
|
||||
return truncateBody(bodyText)
|
||||
} catch (_: Exception) {
|
||||
return "(读取请求体失败)\n"
|
||||
}
|
||||
}
|
||||
|
||||
/// 响应体读取:从 SimApiResponseWriter 缓存取完整响应体;未捕获到时返回空串
|
||||
/// (长度与截断由调用方处理)
|
||||
private func readResponseBody(context: HttpContext): String {
|
||||
match (context.items.get(SimApiResponseWriter.responseBodyCacheKey)) {
|
||||
case Some(v) => if (let s: String <- v) { s } else { "" }
|
||||
case None => ""
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求体截断:JSON 字段级截断,非 JSON 则整串截断
|
||||
private func truncateBody(body: String): String {
|
||||
let maxLen = _options.simApiRequestLogOptions.requestStringLogLength
|
||||
if (maxLen <= 0) {
|
||||
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) {
|
||||
}
|
||||
// 非 JSON 或解析失败:整串按长度截断
|
||||
if (body.size <= maxLen) {
|
||||
return body + "\n"
|
||||
}
|
||||
return body[0..maxLen] + "...(${body.size})\n"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user