feat(swap): SwapCalcTrace 支持运行时按请求开关 + 响应头可见(无需重启)
- SwapCalcTrace 改为双模式:保留全局静态开关(单元测试用 IsEnabled/Dump),
新增请求级 AsyncLocal 缓冲(EnableForRequest/DumpForRequest/ClearForRequest),
由中间件按请求开关,不跨请求串扰。
- 新增 SwapCalcTraceMiddleware:开启方式三选一——URL ?swaptrace=1 /
请求头 X-SwapCalc-Trace:1 / 标记文件 App_Data/swapcalctrace.on(无需重启);
Response.OnStarting 时把逐日计息过程写入响应头 X-SwapCalc-Trace(超长落
logs/swapcalctrace-YYYYMMDD.log 并在头给路径),同时写服务器日志。
开发者在浏览器“网络”面板即可查看,零前端改码、零行为影响、性能可忽略。
- Program.cs 在 ExceptionMiddleware 后注册该中间件。
- 删除 CalcDailyCompoundInterestByEod 遗留垃圾日志 LogFactory.GetLogger("test").Error("lksafhasdhfjas")。
- CalcDailySimpleInterest 两处追踪守卫改为 IsEnabled||IsRequestEnabled。
- 既有 PrepaidPrincipalCloseTraceTest / ClosingChainTraceTest 2/2 仍全绿。
回答用户两点:① dynomicPrincipal 公式字段含义;② 原 Trace 需重启才能开,
现改为前端 otcdebug 式(?swaptrace=1) 即可在响应头看到逐步计息过程。
This commit is contained in:
@@ -5,48 +5,58 @@ using System.Text;
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 计息计算过程追踪器(默认关闭,零运行时成本)。
|
||||
/// 计息计算过程追踪器。默认完全关闭,零运行时成本;开启后逐日记录:
|
||||
/// 计息起点、日终归档 ValueDate 地板、计息基数 dynomicPrincipal、当日利率、当日利息、累计利息。
|
||||
/// 便于定位"算出来一个数却不对"的根因,也便于把一次真实平仓的逐步过程打印出来与 Excel 对账。
|
||||
///
|
||||
/// 在 CalcDailySimpleInterest / CalcDailyCompoundInterest 等计息函数中调用
|
||||
/// SwapCalcTrace.Record(...),开启后逐日记录:计息起点、日终归档 ValueDate 地板、
|
||||
/// 计息基数、当日利率、当日利息、累计利息。便于定位"算出来一个数却不对"的根因,
|
||||
/// 也便于把一次真实平仓的逐步过程打印出来与 Excel 对账。
|
||||
///
|
||||
/// 用法(单元测试或临时排障):
|
||||
/// SwapCalcTrace.IsEnabled = true;
|
||||
/// SwapCalcTrace.Reset();
|
||||
/// ... 调用计息 ...
|
||||
/// Console.WriteLine(SwapCalcTrace.Dump());
|
||||
/// SwapCalcTrace.IsEnabled = false;
|
||||
/// 两种开启方式(互不冲突):
|
||||
/// 1) 全局(单元测试):SwapCalcTrace.IsEnabled = true; SwapCalcTrace.Reset(); ...; SwapCalcTrace.Dump();
|
||||
/// 2) 请求(运行时排障,无需重启):经 SwapCalcTraceMiddleware,在 URL 追加 ?swaptrace=1
|
||||
/// 或请求头 X-SwapCalc-Trace: 1 即可,追踪文本会出现在响应头 X-SwapCalc-Trace(超长落 logs/ 并在头部给路径)。
|
||||
/// </summary>
|
||||
public static class SwapCalcTrace
|
||||
{
|
||||
// ---- 全局模式(单元测试,保持 ThreadStatic 兼容既有测试) ----
|
||||
public static bool IsEnabled { get; set; } = false;
|
||||
|
||||
[ThreadStatic]
|
||||
private static List<string> _lines;
|
||||
private static List<string> _globalLines;
|
||||
|
||||
private static List<string> Lines => _lines ??= new List<string>();
|
||||
private static List<string> GlobalLines => _globalLines ??= new List<string>();
|
||||
|
||||
public static void Reset() => Lines.Clear();
|
||||
// ---- 请求模式(运行时排障,按请求异步流隔离,不会跨请求串扰) ----
|
||||
private static readonly AsyncLocal<StringBuilder> _reqBuf = new AsyncLocal<StringBuilder>();
|
||||
private static readonly AsyncLocal<bool> _reqOn = new AsyncLocal<bool>();
|
||||
|
||||
public static void Header(string title)
|
||||
public static void EnableForRequest(bool on = true) => _reqOn.Value = on;
|
||||
public static bool IsRequestEnabled => _reqOn.Value;
|
||||
public static void ClearForRequest() => _reqBuf.Value = null;
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
if (IsEnabled) Lines.Add($"== {title} ==");
|
||||
GlobalLines.Clear();
|
||||
_reqBuf.Value = null;
|
||||
}
|
||||
|
||||
public static void Line(string text)
|
||||
{
|
||||
if (IsEnabled) Lines.Add(text);
|
||||
}
|
||||
public static void Header(string title) => Record($"== {title} ==");
|
||||
public static void Line(string text) => Record(text);
|
||||
|
||||
/// <summary>记录某一计息日的明细。</summary>
|
||||
public static void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated)
|
||||
=> Record($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}");
|
||||
|
||||
private static void Record(string s)
|
||||
{
|
||||
if (IsEnabled)
|
||||
Lines.Add($" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}");
|
||||
if (IsEnabled) GlobalLines.Add(s);
|
||||
if (_reqOn.Value)
|
||||
{
|
||||
_reqBuf.Value ??= new StringBuilder();
|
||||
_reqBuf.Value.AppendLine(s);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Dump() => string.Join(Environment.NewLine, Lines);
|
||||
public static string Dump() => string.Join(Environment.NewLine, GlobalLines);
|
||||
|
||||
public static string DumpForRequest() => _reqBuf.Value?.ToString() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1360,7 +1360,7 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
decimal tdDynomicPrincipal = dynomicPrincipal;
|
||||
var calcDays = (endDate - startDate).Days;
|
||||
if (SwapCalcTrace.IsEnabled)
|
||||
if (SwapCalcTrace.IsEnabled || SwapCalcTrace.IsRequestEnabled)
|
||||
{
|
||||
SwapCalcTrace.Header($"CalcDailySimpleInterest posId={position.id} mode={position.InterestMode} type={(position.InterestType == (int)InterestTypeEnum.复利 ? "复利" : "单利")}");
|
||||
SwapCalcTrace.Line($" PosiStartDate={startDate:yyyy-MM-dd} endDate={endDate:yyyy-MM-dd} calcDays={calcDays} calcFirst={calcFirst} calcLast={calcLast}");
|
||||
@@ -1409,7 +1409,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
interest += interest1;
|
||||
tdinterest += tdinterest1;
|
||||
if (SwapCalcTrace.IsEnabled)
|
||||
if (SwapCalcTrace.IsEnabled || SwapCalcTrace.IsRequestEnabled)
|
||||
SwapCalcTrace.Day(i, accrueDate, (decimal)floatRate, flowEvent.InterestPrincipal, interest1, interest);
|
||||
}
|
||||
}
|
||||
@@ -1438,7 +1438,6 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal tdDynomicPrincipal = posiPrincipal;
|
||||
double floatRate = Convert.ToDouble(floateRate);
|
||||
var days = (endDate - tradeDate).Days;
|
||||
LogFactory.GetLogger("test").Error("lksafhasdhfjas");
|
||||
if (days % interestPeriod == 0)
|
||||
{
|
||||
LogFactory.GetLogger("test").Error("kluausdyfh");
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.SwapModule;
|
||||
|
||||
namespace YLErp.Web.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// 计息计算过程追踪中间件(运行时排障用,无需重启、无需改前端)。
|
||||
///
|
||||
/// 开启方式(三选一):
|
||||
/// 1) 请求 URL 追加 ?swaptrace=1
|
||||
/// 2) 请求头带 X-SwapCalc-Trace: 1
|
||||
/// 3) 站点根目录 App_Data/swapcalctrace.on 标记文件存在(对所有请求开启,谨慎使用)
|
||||
///
|
||||
/// 开启后,SwapCalcTrace 会在计息函数中逐日记录过程。本中间件在响应发出前把追踪文本写入:
|
||||
/// - 响应头 X-SwapCalc-Trace(开发者在浏览器"网络"面板即可看到,超长则写入日志并在头部给文件路径)
|
||||
/// - 服务器日志 logs/swapcalctrace-YYYYMMDD.log(完整文本,便于 tail/检索)
|
||||
///
|
||||
/// 性能影响可忽略(仅被显式开启的少数请求触发,且每次只是拼字符串)。
|
||||
/// </summary>
|
||||
public class SwapCalcTraceMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
|
||||
public SwapCalcTraceMiddleware(RequestDelegate next, IWebHostEnvironment env)
|
||||
{
|
||||
_next = next;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
bool on = context.Request.Query["swaptrace"] == "1"
|
||||
|| context.Request.Headers["X-SwapCalc-Trace"] == "1";
|
||||
if (!on) on = MarkerFileExists();
|
||||
if (on) SwapCalcTrace.EnableForRequest(true);
|
||||
|
||||
try
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
// OnStarting 在响应体写出前触发,确保整个请求(含计息)都已被追踪
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
FlushTrace(context);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
await _next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (on) SwapCalcTrace.ClearForRequest();
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushTrace(HttpContext context)
|
||||
{
|
||||
var trace = SwapCalcTrace.DumpForRequest();
|
||||
if (string.IsNullOrEmpty(trace)) return;
|
||||
|
||||
var logger = LogFactory.GetLogger("SwapCalcTrace");
|
||||
logger.Info($"[SwapCalcTrace] {context.Request.Path}\n{trace}");
|
||||
|
||||
var headerVal = trace;
|
||||
if (trace.Length > 4000)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.Combine(_env.ContentRootPath, "logs");
|
||||
Directory.CreateDirectory(dir);
|
||||
var file = Path.Combine(dir, $"swapcalctrace-{DateTime.Now:yyyyMMdd}.log");
|
||||
File.AppendAllText(file,
|
||||
$"=== {DateTime.Now:yyyy-MM-dd HH:mm:ss} {context.Request.Path} ===\n{trace}\n\n");
|
||||
headerVal = $"(trace truncated, full in {file})\n" + trace.Substring(0, 4000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
headerVal = trace.Substring(0, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
try { context.Response.Headers["X-SwapCalc-Trace"] = headerVal; } catch { }
|
||||
}
|
||||
|
||||
private bool MarkerFileExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(Path.Combine(_env.ContentRootPath, "App_Data", "swapcalctrace.on"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,9 @@ try
|
||||
|
||||
app.UseMiddleware<ExceptionMiddleware>();
|
||||
|
||||
// 计息计算过程追踪(?swaptrace=1 / X-SwapCalc-Trace:1 开启,详见 SwapCalcTraceMiddleware)
|
||||
app.UseMiddleware<SwapCalcTraceMiddleware>();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
// Linux 路径区分大小写,需要对静态文件做处理
|
||||
|
||||
Reference in New Issue
Block a user