如何在Azure托管的ASP.NET Core中记录响应总大小
如何在ASP.NET Core中记录响应总大小(含头部+压缩后体)
嘿,这个需求我之前做类似成本核算的时候刚好研究过,给你梳理下靠谱的实现方案和关键细节,帮你准确统计出站响应的总大小。
首先先明确你几个疑问的答案:
HttpContext.Response.Body.Length不包含响应头部,而且如果启用了响应压缩,这个值要么是压缩前的原始大小,要么根本没被正确设置(比如流式响应场景),所以直接用它来统计总大小是不准确的。- 要统计真实的出站数据大小,必须同时计算响应头部的字节数和实际发送的响应体字节数(包括压缩后的)。
核心思路
我们需要包装ASP.NET Core的响应流,统计所有写入到客户端的字节数;同时手动计算响应头部的字节数,两者相加就是总出站大小。这个方案能兼容响应压缩、流式响应等所有场景。
代码实现
1. 自定义计数流和中间件
先写一个用来统计写入字节数的包装流,再写中间件替换响应流并计算总大小:
public class ResponseSizeLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<ResponseSizeLoggingMiddleware> _logger; public ResponseSizeLoggingMiddleware(RequestDelegate next, ILogger<ResponseSizeLoggingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { // 获取原始的响应体特性 var originalResponseBodyFeature = context.Features.Get<IHttpResponseBodyFeature>(); if (originalResponseBodyFeature == null) { await _next(context); return; } // 用计数流包装原始响应流 var countingStream = new CountingStream(originalResponseBodyFeature.Stream); context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(countingStream)); try { // 让后续中间件/控制器处理请求 await _next(context); // 确保响应写入完成 await originalResponseBodyFeature.CompleteAsync(); // 计算响应头部的字节大小 var headersSize = CalculateHeadersSize(context.Response.Headers); // 总大小 = 头部 + 体 var totalResponseSize = headersSize + countingStream.TotalBytesWritten; // 这里可以根据需求过滤特定功能的请求,比如Blob下载路径 if (context.Request.Path.StartsWithSegments("/api/blob/download")) { _logger.LogInformation("Blob download response size: {TotalSize} bytes (Headers: {HeadersSize}, Body: {BodySize})", totalResponseSize, headersSize, countingStream.TotalBytesWritten); // 把数据存入数据库/监控系统,用于成本核算 } } finally { // 恢复原始响应体特性 context.Features.Set(originalResponseBodyFeature); } } private long CalculateHeadersSize(IHeaderDictionary headers) { if (headers == null || !headers.Any()) return 0; // HTTP头部格式:每个键值对为 "Key: Value\r\n",最后加"\r\n"表示头部结束 var headersString = string.Join("\r\n", headers.Select(kv => $"{kv.Key}: {kv.Value}")) + "\r\n\r\n"; // HTTP头部默认用UTF-8编码计算字节数 return System.Text.Encoding.UTF8.GetByteCount(headersString); } } // 自定义计数流,统计写入的字节数 public class CountingStream : Stream { private readonly Stream _innerStream; public long TotalBytesWritten { get; private set; } public CountingStream(Stream innerStream) { _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); } public override bool CanRead => _innerStream.CanRead; public override bool CanSeek => _innerStream.CanSeek; public override bool CanWrite => _innerStream.CanWrite; public override long Length => _innerStream.Length; public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } public override void Flush() => _innerStream.Flush(); public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); public override void SetLength(long value) => _innerStream.SetLength(value); public override void Write(byte[] buffer, int offset, int count) { TotalBytesWritten += count; _innerStream.Write(buffer, offset, count); } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { TotalBytesWritten += count; await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); } protected override void Dispose(bool disposing) { if (disposing) _innerStream.Dispose(); base.Dispose(disposing); } }
2. 注册中间件
在Program.cs中注册这个中间件,注意顺序要放在任何可能修改响应体的中间件之后(比如UseResponseCompression、UseStaticFiles),但要在UseEndpoints之前:
var builder = WebApplication.CreateBuilder(args); // 添加响应压缩服务(如果需要) builder.Services.AddResponseCompression(); builder.Services.AddControllers(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseStaticFiles(); // 如果启用了响应压缩,要先注册它 app.UseResponseCompression(); app.UseRouting(); app.UseAuthorization(); // 注册我们的响应大小记录中间件 app.UseMiddleware<ResponseSizeLoggingMiddleware>(); app.MapControllers(); app.Run();
关键细节说明
- 压缩后的体大小统计:这个方案通过包装原始响应流,不管后续中间件(比如响应压缩)怎么处理,所有写入到客户端的字节都会被计数,所以拿到的是真实的压缩后体大小。
- 响应头部的计算:HTTP头部也是出站流量的一部分,Azure会统计这部分字节,所以必须把它加上。我们通过将头部转换成标准HTTP格式的字符串,再计算字节数,这个方式是准确的。
- 特定功能过滤:你可以通过
context.Request.Path、查询参数或者自定义请求标识,只记录Blob下载相关的响应,避免不必要的统计。 - 流式响应兼容:如果你的Blob下载是用流式传输(比如直接写Blob流到响应),这个计数流依然能准确统计所有发送的字节,没有问题。
注意事项
- 性能开销:这个中间件的性能影响极小,只是做了流的包装和字节计数,不会对响应速度造成明显影响。
- Azure成本核算:Azure的出站流量是按实际发送的字节数收费的,这个方案统计的总大小和Azure的计费逻辑一致,可以直接用来核算成本。
内容的提问来源于stack exchange,提问作者Adam Stapleton
相关产品推荐
相关产品推荐

