Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ Notes](../../RELEASENOTES.md).
OpenMetrics and a start time is available.
([#7223](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7223))

* GZip compress scrape endpoint responses when `Accept-Encoding: gzip` is
specified by the HTTP request headers.
([#7274](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7274))

## 1.15.3-beta.1

Released 2026-Apr-21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageTags>$(PackageTags);prometheus;metrics</PackageTags>
<MinVerTagPrefix>coreunstable-</MinVerTagPrefix>
<DefineConstants>$(DefineConstants);PROMETHEUS_ASPNETCORE</DefineConstants>
<NoWarn>$(NoWarn);CA2007</NoWarn>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AspNetCore has no synchronisation context so ConfigureAwait(false) has no effect.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I would consider keeping this calls just in case we need to share with httplistener similar code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment the code reuse is one way from HttpListener to AspNetCore, so I don't think we need to worry about that in practice as if it needs to be shared it'll go in the other project which doesn't supress CA2007.

</PropertyGroup>

<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Compression;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Net.Http.Headers;
using OpenTelemetry.Exporter.Prometheus;
using OpenTelemetry.Internal;
Expand Down Expand Up @@ -73,9 +75,11 @@ public async Task InvokeAsync(HttpContext httpContext)

using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(requestCancelled.Token, httpContext.RequestAborted);

var protocol = Negotiate(httpContext.Request);
var requestHeaders = httpContext.Request.GetTypedHeaders();

var collectionResponse = await this.exporter.CollectionManager.EnterCollect(protocol.IsOpenMetrics).ConfigureAwait(false);
var protocol = Negotiate(requestHeaders);

var collectionResponse = await this.exporter.CollectionManager.EnterCollect(protocol.IsOpenMetrics);

try
{
Expand All @@ -90,7 +94,7 @@ public async Task InvokeAsync(HttpContext httpContext)
response.Headers.Append("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
response.ContentType = PrometheusProtocol.GetContentType(protocol);

await response.Body.WriteAsync(dataView.Array.AsMemory(0, dataView.Count), linkedCts.Token).ConfigureAwait(false);
await WriteResponseAsync(response, dataView.Array.AsMemory(0, dataView.Count), AcceptsGZip(requestHeaders), linkedCts.Token);
}
else
{
Expand Down Expand Up @@ -144,9 +148,9 @@ static bool TryGetScrapeTimeout(
}
}

internal static PrometheusProtocol Negotiate(HttpRequest request)
internal static PrometheusProtocol Negotiate(RequestHeaders headers)
{
var acceptHeader = request.GetTypedHeaders().Accept;
var acceptHeader = headers.Accept;

if (acceptHeader is not { Count: > 0 })
{
Expand Down Expand Up @@ -284,4 +288,46 @@ private static bool TryParse(
protocol = new(mediaType, escaping, version, isOpenMetrics);
return true;
}

private static bool AcceptsGZip(RequestHeaders headers)
{
if (headers.AcceptEncoding is { Count: > 0 } acceptEncoding)
{
foreach (var parameter in acceptEncoding)
{
if (parameter.Quality is not 0 && parameter.Value.Equals("gzip", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}

return false;
}

private static async Task WriteResponseAsync(
HttpResponse response,
ReadOnlyMemory<byte> content,
bool compress,
CancellationToken cancellationToken)
{
Comment thread
martincostello marked this conversation as resolved.
response.Headers.AppendCommaSeparatedValues(HeaderNames.Vary, HeaderNames.AcceptEncoding);

if (compress)
{
response.Headers.Append(HeaderNames.ContentEncoding, "gzip");

await using var gzip = new GZipStream(
response.Body,
CompressionLevel.Fastest,
leaveOpen: true);

await gzip.WriteAsync(content, cancellationToken);
await gzip.FlushAsync(cancellationToken);
}
else
{
await response.BodyWriter.WriteAsync(content, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ public void PrometheusExporterMiddlewareNegotiate_UsesTypedAcceptHeaders(
var context = new DefaultHttpContext();
context.Request.Headers.Accept = accept;

var actual = PrometheusExporterMiddleware.Negotiate(context.Request);
var actual = PrometheusExporterMiddleware.Negotiate(context.Request.GetTypedHeaders());

Assert.Equal(mediaType, actual.MediaType);
Assert.Equal(isOpenMetrics, actual.IsOpenMetrics);
Expand All @@ -252,7 +252,7 @@ public void PrometheusExporterMiddlewareNegotiate_UsesFallbackForInvalidHeader(s
var context = new DefaultHttpContext();
context.Request.Headers.Accept = accept;

var actual = PrometheusExporterMiddleware.Negotiate(context.Request);
var actual = PrometheusExporterMiddleware.Negotiate(context.Request.GetTypedHeaders());

Assert.Equivalent(PrometheusProtocol.Fallback, actual);
}
Expand Down Expand Up @@ -552,6 +552,7 @@ private static async Task VerifyAsync(
"text/plain; version=0.0.4; charset=utf-8";

Assert.Equal(contentType, response.Content.Headers.ContentType!.ToString());
Assert.Equal(["Accept-Encoding"], response.Headers.Vary);

var additionalTags = meterTags is { Length: > 0 }
? $"{string.Join(",", meterTags.Select(x => $"{x.Key}=\"{x.Value}\""))},"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.IO.Compression;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
Expand Down Expand Up @@ -71,13 +72,63 @@ public async Task Scrape_Endpoint_Returns_No_Content_If_Sdk_Disabled()
Assert.Empty(content);
}

[Fact]
public async Task Scrape_Endpoint_Uses_GZip_When_Requested()
{
// Arrange
var builder = WebApplication.CreateBuilder();

// Listen on any available port
builder.WebHost.UseUrls("http://127.0.0.1:0");

builder.Services
.AddOpenTelemetry()
.WithMetrics((builder) => builder.AddPrometheusExporter());

using var app = builder.Build();

app.MapPrometheusScrapingEndpoint();

await app.StartAsync();

var server = app.Services.GetRequiredService<IServer>();
var addresses = server.Features.Get<IServerAddressesFeature>();

var baseAddress = addresses!.Addresses
.Select((p) => new Uri(p))
.Last();

using var httpClient = new HttpClient();

httpClient.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");

using var response = await httpClient.GetAsync(new Uri(baseAddress, "metrics"));

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotNull(response.Content.Headers.ContentEncoding);
Assert.Equal<string>(["gzip"], response.Content.Headers.ContentEncoding);
Assert.Equal(["Accept-Encoding"], response.Headers.Vary);
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain; version=0.0.4; charset=utf-8", response.Content.Headers.ContentType.ToString());

using var compressed = await response.Content.ReadAsStreamAsync();
using var decompressed = new GZipStream(compressed, CompressionMode.Decompress);
using var reader = new StreamReader(decompressed);

var content = await reader.ReadToEndAsync();

Assert.NotEmpty(content);
Assert.EndsWith("# EOF\n", content, StringComparison.Ordinal);
}

[EnabledOnDockerPlatformTheory(DockerPlatform.Linux)]
[InlineData("")]
[InlineData("OpenMetricsText0.0.1")]
[InlineData("OpenMetricsText1.0.0")]
[InlineData("PrometheusText0.0.4")]
[InlineData("PrometheusText1.0.0")]

public async Task Prometheus_Can_Scrape_Metrics(string scrapeProtocol) => await GenerateMetricsAsync(async (baseAddress) =>
{
// Arrange
Expand Down
Loading