using System.Diagnostics; using Application.Abstractions.Data; using Domain.Entities.Developers; namespace Web.Api.Middleware; /// /// 공개 API (/v1/*, /oauth/*) 호출 로그 — ApiRequestLog 테이블 기록. /// 사용량 통계(UsageStats/GlobalStats)의 원천이자 결제 보고 분쟁 시 감사 추적 근거. /// 요청 scope 의 DbContext 를 쓰지 않고 자체 scope 를 만들어 핸들러 변경분 오염을 방지하며, /// 기록 실패는 API 응답에 영향을 주지 않는다 (경고 로그만). /// public sealed class ApiRequestLogMiddleware( RequestDelegate next, IServiceScopeFactory scopeFactory, ILogger logger ) { public async Task InvokeAsync(HttpContext context) { var path = context.Request.Path; if (!path.StartsWithSegments("/v1") && !path.StartsWithSegments("/oauth")) { await next(context); return; } var sw = Stopwatch.StartNew(); try { await next(context); } finally { sw.Stop(); try { int? applicationID = null; long? patID = null; var tokenType = context.User.FindFirst("token_type")?.Value; if (tokenType == "oauth2" && int.TryParse(context.User.FindFirst("app_id")?.Value, out var appID)) { applicationID = appID; } else if (tokenType == "pat" && long.TryParse(context.User.FindFirst("token_id")?.Value, out var pat)) { patID = pat; } using var scope = scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.ApiRequestLog.Add(ApiRequestLog.Create( applicationID, patID, path.Value ?? string.Empty, context.Request.Method, context.Response.StatusCode, (int)sw.ElapsedMilliseconds, context.Connection.RemoteIpAddress?.ToString() )); await db.SaveChangesAsync(CancellationToken.None); } catch (Exception ex) { logger.LogWarning(ex, "[ApiRequestLog] 호출 로그 기록 실패 — {Path}", path); } } } }