| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- using System.Diagnostics;
- using Application.Abstractions.Data;
- using Domain.Entities.Developers;
- namespace Web.Api.Middleware;
- /// <summary>
- /// 공개 API (/v1/*, /oauth/*) 호출 로그 — ApiRequestLog 테이블 기록.
- /// 사용량 통계(UsageStats/GlobalStats)의 원천이자 결제 보고 분쟁 시 감사 추적 근거.
- /// 요청 scope 의 DbContext 를 쓰지 않고 자체 scope 를 만들어 핸들러 변경분 오염을 방지하며,
- /// 기록 실패는 API 응답에 영향을 주지 않는다 (경고 로그만).
- /// </summary>
- public sealed class ApiRequestLogMiddleware(
- RequestDelegate next,
- IServiceScopeFactory scopeFactory,
- ILogger<ApiRequestLogMiddleware> 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<IAppDbContext>();
- 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);
- }
- }
- }
- }
|