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

124 lines
4.1 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.communications.*
import simapi.configurations.*
/**
* 请求日志中间件:记录请求方法、URL、请求头、请求体、响应状态码与耗时。
* 对应 C# 的 SimApi.Middlewares.SimApiRequestLogMiddleware。
*/
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))
// 调用下一级
next(context)
// 响应信息
let elapsed = MonoTime.now() - start
let elapsedMs = elapsed / Duration.millisecond
sb.append("*( Response [${context.response.statusCode}] ) => ${elapsedMs}ms\n")
_logger.info(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("\"${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"
}
}
private func truncateBody(body: String): String {
if (_options.simApiRequestLogOptions.requestStringLogLength <= 0 ||
body.size <= _options.simApiRequestLogOptions.requestStringLogLength) {
return body + "\n"
}
// 简单按长度截断(不做 JSON 字段级截断,保持实现简洁)
let chars = body.toArray()
var sb = StringBuilder()
for (i in 0.._options.simApiRequestLogOptions.requestStringLogLength) {
sb.append(chars[i])
}
return "${sb.toString()}...(${body.size})\n"
}
}