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

235 lines
8.7 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.
* 遵循 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"
}
}