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);
}
}
}
}