Bạn có thể thêm ghi log yêu cầu (request logging) vào cơ sở dữ liệu trong một ASP.NET Core Web API. Và cách để thực hiện điều đó là thêm middleware.
Mục lục
Thêm một lớp middleware
Bạn tạo một lớp middleware mới. Khi chúng ta thêm request log vào cơ sở dữ liệu, chúng ta sẽ tính toán thời gian yêu cầu mất bao lâu để nhận được phản hồi. Để làm điều đó, chúng ta sẽ tạo một instance Stopwatch mới và bắt đầu bộ đếm thời gian.
Để tính toán thời gian yêu cầu hoàn thành và thêm nó vào cơ sở dữ liệu, chúng ta sẽ thêm một delegate sau khi phản hồi đã được hoàn thành. Chúng ta có thể thực hiện điều đó bằng cách gọi httpContext.Response.OnCompleted trong middleware.
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private const string ResponseTimer = "ResponseTimer";
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(
RequestDelegate next
)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
httpContext.Items.TryAdd(ResponseTimer, stopwatch);
httpContext.Response.OnCompleted(async () =
{
Stopwatch? responseTimer = null;
if (httpContext.Items.TryGetValue(ResponseTimer, out var timer))
{
responseTimer = timer as Stopwatch;
if (responseTimer != null)
{
// Đã tìm thấy timer, dừng nó lại.
responseTimer.Stop();
}
}
});
// Middleware tiếp theo
await _next(httpContext);
}
}
Chúng ta phải nhớ đăng ký middleware trong Web API. Để làm điều đó, hãy vào Program.cs và gọi phương thức mở rộng AddMiddleware:
// Program.cs
var app = builder.Build();
app.UseMiddleware<RequestLoggingMiddleware>();
Nên thêm middleware này sau khi builder.Build được gọi để nó nằm ở vị trí cao trong chuỗi ưu tiên middleware.
Thiết lập cơ sở dữ liệu
Để lưu dữ liệu yêu cầu vào cơ sở dữ liệu, bạn cần thiết lập cơ sở dữ liệu trước. Và chúng ta sẽ sử dụng SQL Server và Entity Framework Core để thực hiện việc đó.
Bạn sẽ cần thêm các gói NuGet sau vào ứng dụng của mình:
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
Bạn cũng cần cài đặt SQL Server và SQL Server Management Studio để có thể tạo và xem cơ sở dữ liệu.
Cấu hình cơ sở dữ liệu trong ứng dụng
Đây là entity mà chúng ta sẽ sử dụng để ánh xạ tới bảng RequestLogging trong cơ sở dữ liệu.
// RequestLoggingEntity.cs
public class RequestLoggingEntity
{
public int Id { get; set; }
public DateTime Date { get; set; }
public string Method { get; set; } = string.Empty;
public string EncodedPathAndQuery { get; set; } = string.Empty;
public string? IpAddress { get; set; }
public string? UserAgent { get; set; }
public int ResponseCode { get; set; }
public TimeSpan? LoadTime { get; set; }
}
Và entity đó sẽ cần được cấu hình để các thuộc tính kiểu chuỗi có độ dài tối đa được thiết lập.
// RequestLoggingConfiguration.cs
public class RequestLoggingConfiguration
: IEntityTypeConfiguration<RequestLoggingEntity>
{
public void Configure(EntityTypeBuilder<RequestLoggingEntity> builder)
{
builder.HasKey(s = s.Id);
builder.Property(s = s.Date)
.HasColumnName("DateUtc")
.HasColumnType("datetime")
.HasConversion(new DateTimeUtcConverter());
builder.Property(s = s.Method)
.HasMaxLength(7);
builder.Property(s = s.EncodedPathAndQuery)
.HasMaxLength(300);
builder.Property(s = s.IpAddress)
.HasMaxLength(30);
builder.Property(s = s.UserAgent)
.HasMaxLength(300);
builder.Property(s = s.ResponseCode);
builder.Property(s = s.LoadTime)
.HasColumnType("time(3)");
builder.ToTable("RequestLogging");
}
}
Ngoài ra, chúng ta đã tạo một lớp DateTimeUtcConverter. Đây là một value converter sẽ lưu thời gian yêu cầu dưới dạng UTC trong cơ sở dữ liệu và sau đó chuyển đổi nó trở lại múi giờ địa phương trong ứng dụng.
// DateTimeUtcConverter.cs
public class DateTimeUtcConverter : ValueConverter<DateTime, DateTime>
{
public DateTimeUtcConverter() :
base(d = d.ToUniversalTime(), d = DateTime.SpecifyKind(d, DateTimeKind.Utc).ToLocalTime()) {}
}
Chúng ta đã thiết lập một lớp repository sẽ gọi DbContext và lưu request log vào cơ sở dữ liệu.
// IRequestLoggingRepository.cs
public interface IRequestLoggingRepository
{
Task Create(CreateRequestLogDto createRequestLog);
}
// RequestLoggingRepository.cs
public class RequestLoggingRepository : IRequestLoggingRepository
{
private readonly RequestLoggingDbContext _dbContext;
public RequestLoggingRepository(RequestLoggingDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Create(CreateRequestLogDto createRequestLog)
{
await _dbContext.RequestLogging.AddAsync(new RequestLoggingEntity
{
Date = DateTime.Now,
Method = TruncateString(createRequestLog.Method, 7),
EncodedPathAndQuery = TruncateString(createRequestLog.EncodedPathAndQuery, 300),
IpAddress = TruncateNullableString(createRequestLog.IpAddress, 30),
UserAgent = TruncateNullableString(createRequestLog.UserAgent, 300),
ResponseCode = (int)createRequestLog.ResponseCode,
LoadTime = createRequestLog.LoadTime
});
await _dbContext.SaveChangesAsync();
}
private string? TruncateNullableString(string? value, int maxCharacters)
{
return (value?.Length ?? 0) maxCharacters ? value?[..maxCharacters] : value;
}
private string TruncateString(string value, int maxCharacters)
{
return TruncateNullableString(value, maxCharacters) ?? string.Empty;
}
}
Chúng ta đã thêm một vài phương thức private để cắt ngắn chuỗi. Điều này nhằm đảm bảo rằng chúng không vượt quá giới hạn độ dài tối đa khi được lưu vào cơ sở dữ liệu, nếu không một exception sẽ được ném ra.
Repository này sẽ cần được đăng ký như một scoped service trong dependency injection.
// Program.cs
builder.Services.AddScoped<IRequestLoggingRepository,
RequestLoggingRepository>();
Trong phương thức Create, chúng ta mong đợi một tham số kiểu CreateRequestLogDto. DTO này sẽ lưu trữ thông tin chi tiết về yêu cầu mà chúng ta lấy được từ request, chẳng hạn như phương thức (method), đường dẫn yêu cầu (request path) và địa chỉ IP.
// CreateRequestLogDto.cs
public class CreateRequestLogDto
{
public required string Method { get; init; } = string.Empty;
public required string EncodedPathAndQuery { get; init; } = string.Empty;
public required string? IpAddress { get; init; }
public required string? UserAgent { get; init; }
public required HttpStatusCode ResponseCode { get; init; }
public required TimeSpan? LoadTime { get; init; }
}
Lớp RequestLoggingDbContext là DbContext sẽ được sử dụng để lưu request log vào cơ sở dữ liệu.
// RequestLoggingDbContext.cs
public class RequestLoggingDbContext : DbContext
{
public required DbSet<RequestLoggingEntity> RequestLogging { get; set; }
public RequestLoggingDbContext()
{
}
public RequestLoggingDbContext(
DbContextOptions<RequestLoggingDbContext> options
) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(RequestLoggingDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
}
Chúng ta đã ghi đè phương thức OnModelCreating để có thể áp dụng các cấu hình từ bất kỳ lớp nào triển khai interface IEntityTypeConfiguration. Điều này có nghĩa là nó sẽ áp dụng cấu hình trong RequestLoggingConfiguration cho bảng RequestLogging trong cơ sở dữ liệu.
Bạn sẽ cần thêm connection string vào appsettings.json để ứng dụng có thể kết nối đến cơ sở dữ liệu. Connection string bên dưới giả định rằng bạn đang sử dụng Local DB, cơ sở dữ liệu có tên là RequestLogging và bạn đang sử dụng integrated security. Nếu cơ sở dữ liệu của bạn nằm trên một host khác, có tên khác, hoặc bạn không sử dụng integrated security, bạn sẽ cần sửa đổi connection string cho phù hợp.
{
"ConnectionStrings": {
"RequestLoggingDbContext": "Server=(localdb)\\MSSQLLocalDB; Database=RequestLogging; Trusted_Connection=true; Trust Server Certificate=true; MultipleActiveResultSets=true; Integrated Security=true;"
}
}
Sau đó, bạn sẽ cần cấu hình điều đó trong Program.cs bằng cách gọi phương thức mở rộng AddDbContext:
// Program.cs
builder.Services.AddDbContext<RequestLoggingDbContext>(options =&
{
options.UseSqlServer(builder.Configuration.GetConnectionString(nameof(RequestLoggingDbContext)));
});
Bạn sẽ cần thêm bảng RequestLogging vào cơ sở dữ liệu. Bạn có thể sử dụng Entity Framework Core migrations nếu bạn cảm thấy thoải mái với cách đó. Hoặc bạn có thể chạy script cơ sở dữ liệu này.
CREATE TABLE [dbo].[RequestLogging](
[Id] [int] IDENTITY(1,1) NOT NULL,
[DateUtc] [datetime] NOT NULL,
[Method] [nvarchar](7) NOT NULL,
[EncodedPathAndQuery] [nvarchar](300) NOT NULL,
[IpAddress] [nvarchar](30) NULL,
[UserAgent] [nvarchar](300) NULL,
[ResponseCode] [int] NOT NULL,
[LoadTime] [time](3) NULL,
CONSTRAINT [PK_RequestLogging] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
Cuối cùng, chúng ta đã thiết lập một service mà middleware sẽ gọi để tạo request log trong cơ sở dữ liệu. Service này sẽ gọi lớp RequestLoggingRepository để giao tiếp với DbContext và thêm request log vào cơ sở dữ liệu.
// IRequestLoggingService.cs
public interface IRequestLoggingService
{
Task Create(CreateRequestLogDto createRequestLog);
}
// RequestLoggingService.cs
public class RequestLoggingService : IRequestLoggingService
{
private readonly IRequestLoggingRepository _requestLoggingRepository;
public RequestLoggingService(IRequestLoggingRepository requestLoggingRepository)
{
_requestLoggingRepository = requestLoggingRepository;
}
public async Task Create(CreateRequestLogDto createRequestLog)
{
await _requestLoggingRepository.Create(createRequestLog);
}
}
Service này sẽ cần được đăng ký trong dependency injection như một scoped service.
// Program.cs
builder.Services.AddScoped<IRequestLoggingService,
RequestLoggingService>();
Tạo log vào cơ sở dữ liệu
Với cơ sở dữ liệu đã được thiết lập và cấu hình, chúng ta có thể sử dụng middleware để tạo log trong cơ sở dữ liệu. Để làm điều đó, chúng ta cần inject instance IServiceScope vào middleware RequestLogging.
Khi chúng ta thêm một delegate vào yêu cầu đã hoàn thành, chúng ta sẽ tạo một scope mới để có thể lấy các instance của các service mà chúng ta đã thiết lập. Scope trong instance httpContext.RequestServices trả về kết quả không thể dự đoán được sau khi yêu cầu đã hoàn thành, đó là lý do tại sao chúng ta cần tạo một scope mới.
Sau đó, chúng ta chỉ cần thiết lập các thuộc tính cần thiết cho instance CreateRequestLogDto và gọi phương thức Create trong RequestLoggingService.
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private const string ResponseTimer = "ResponseTimer";
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(
IServiceScopeFactory serviceScopeFactory,
RequestDelegate next
)
{
_serviceScopeFactory = serviceScopeFactory;
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
httpContext.Items.TryAdd(ResponseTimer, stopwatch);
httpContext.Response.OnCompleted(async () =
{
using var scope = _serviceScopeFactory.CreateScope();
var requestLoggingService = scope.ServiceProvider.GetRequiredService<IRequestLoggingService>();
Stopwatch? responseTimer = null;
if (httpContext.Items.TryGetValue(ResponseTimer, out var timer))
{
responseTimer = timer as Stopwatch;
if (responseTimer != null)
{
// Đã tìm thấy timer, dừng nó lại.
responseTimer.Stop();
}
}
await requestLoggingService.Create(new CreateRequestLogDto
{
Method = httpContext.Request.Method,
EncodedPathAndQuery = httpContext.Request.GetEncodedPathAndQuery(),
IpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = httpContext.Request.Headers.UserAgent,
ResponseCode = (HttpStatusCode)httpContext.Response.StatusCode,
LoadTime = responseTimer?.Elapsed
});
});
// Middleware tiếp theo
await _next(httpContext);
}
}
Forwarded headers
Nếu bạn đang chạy ứng dụng của mình trên Kubernetes, trên một load balancer hoặc sử dụng reverse proxy như Cloudflare, bạn sẽ cần bật forwarded headers để lấy địa chỉ IP của yêu cầu gốc.
Bạn thực hiện điều đó bằng cách cấu hình ForwardedHeadersOptions trong Program.cs và chỉ định forwarded headers nào bạn muốn cấu hình. Bạn sử dụng enum ForwardedHeaders và có các tùy chọn sau:
ForwardedHeaders.XForwardedFor– Địa chỉ IP nguồn gốc của client sử dụng request headerX-Forwarded-For.ForwardedHeaders.XForwardedHost– Host nguồn gốc của client sử dụng request headerX-Forwarded-Host.ForwardedHeaders.XForwardedProto– Xác định xem client kết nối bằnghttp://hayhttps://. Giá trị này đến từ request headerX-Forwarded-Proto.ForwardedHeaders.XForwardedPrefix– Xác định đường dẫn cơ sở gốc của client sử dụng request headerX-Forwarded-Prefix.ForwardedHeaders.All– Thêm tất cả các forwarded headersFor,Host,ProtovàPrefix.
Đây là một ví dụ về cách bạn có thể cấu hình ứng dụng để đọc request header X-Forwarded-For nhằm lấy địa chỉ IP.
// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =&
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
});
Nếu bạn muốn thêm nhiều forwarded headers nhưng không phải tất cả, bạn có thể phân tách từng header bằng ký hiệu |.
// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =&
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedHost;
});
Bạn cũng sẽ cần gọi UseForwardedHeaders trong Program.cs sau khi ứng dụng đã được build để sử dụng forwarded headers.
// Program.cs
app.UseForwardedHeaders();
Ghi đè request header
Một số phần mềm reverse proxy chỉ định request header riêng của chúng cho các thuộc tính được chuyển tiếp (forwarded). Ví dụ, nếu bạn sử dụng Cloudflare, họ thêm địa chỉ IP nguồn gốc của client vào request header CF-Connecting-IP. Do đó, bạn có thể thiết lập nó trong thuộc tính ForwardedForHeaderName của lớp ForwardedHeadersOptions như sau:
// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =&
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
options.ForwardedForHeaderName = "CF-Connecting-IP";
});
Khi nào không nên bật forwarded headers
Nếu người dùng kết nối trực tiếp đến máy chủ nơi ứng dụng của bạn được lưu trữ và không có load balancer nào được cấu hình, đừng bật forwarded headers.
Nếu không, người dùng sẽ có thể thay đổi địa chỉ IP của yêu cầu bằng cách ghi đè request header. Đây sẽ là một vấn đề bảo mật nghiêm trọng vì người dùng có thể giả mạo danh tính người khác.
Xem video
Khi bạn xem video, bạn sẽ thấy cách chúng tôi đã thêm request logging vào một ASP.NET Core Web API và cách nó hoạt động.
Và khi bạn tải xuống code example, bạn sẽ có thể tự mình thử nghiệm request logging và xem các request log được tạo trong cơ sở dữ liệu.
Xóa logs
Như trường hợp của tất cả các loại logging, log có thể tích tụ dần theo thời gian. Do đó, bạn nên thiết lập một bộ lập lịch (scheduler) để xóa các request log cũ theo các khoảng thời gian định kỳ. Nếu không, bảng RequestLogging sẽ trở nên rất lớn.



