ApiRequestLogMiddleware.cs 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. using System.Diagnostics;
  2. using Application.Abstractions.Data;
  3. using Domain.Entities.Developers;
  4. namespace Web.Api.Middleware;
  5. /// <summary>
  6. /// 공개 API (/v1/*, /oauth/*) 호출 로그 — ApiRequestLog 테이블 기록.
  7. /// 사용량 통계(UsageStats/GlobalStats)의 원천이자 결제 보고 분쟁 시 감사 추적 근거.
  8. /// 요청 scope 의 DbContext 를 쓰지 않고 자체 scope 를 만들어 핸들러 변경분 오염을 방지하며,
  9. /// 기록 실패는 API 응답에 영향을 주지 않는다 (경고 로그만).
  10. /// </summary>
  11. public sealed class ApiRequestLogMiddleware(
  12. RequestDelegate next,
  13. IServiceScopeFactory scopeFactory,
  14. ILogger<ApiRequestLogMiddleware> logger
  15. ) {
  16. public async Task InvokeAsync(HttpContext context)
  17. {
  18. var path = context.Request.Path;
  19. if (!path.StartsWithSegments("/v1") && !path.StartsWithSegments("/oauth"))
  20. {
  21. await next(context);
  22. return;
  23. }
  24. var sw = Stopwatch.StartNew();
  25. try
  26. {
  27. await next(context);
  28. }
  29. finally
  30. {
  31. sw.Stop();
  32. try
  33. {
  34. int? applicationID = null;
  35. long? patID = null;
  36. var tokenType = context.User.FindFirst("token_type")?.Value;
  37. if (tokenType == "oauth2" && int.TryParse(context.User.FindFirst("app_id")?.Value, out var appID))
  38. {
  39. applicationID = appID;
  40. }
  41. else if (tokenType == "pat" && long.TryParse(context.User.FindFirst("token_id")?.Value, out var pat))
  42. {
  43. patID = pat;
  44. }
  45. using var scope = scopeFactory.CreateScope();
  46. var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
  47. db.ApiRequestLog.Add(ApiRequestLog.Create(
  48. applicationID,
  49. patID,
  50. path.Value ?? string.Empty,
  51. context.Request.Method,
  52. context.Response.StatusCode,
  53. (int)sw.ElapsedMilliseconds,
  54. context.Connection.RemoteIpAddress?.ToString()
  55. ));
  56. await db.SaveChangesAsync(CancellationToken.None);
  57. }
  58. catch (Exception ex)
  59. {
  60. logger.LogWarning(ex, "[ApiRequestLog] 호출 로그 기록 실패 — {Path}", path);
  61. }
  62. }
  63. }
  64. }