/* * 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 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 的 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 { 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(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" } }