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 @@ -3,6 +3,8 @@ Microsoft.AspNetCore.Builder.PrometheusExporterEndpointRouteBuilderExtensions
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.get -> bool
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.MaxScrapeResponseSizeBytes.get -> int
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.MaxScrapeResponseSizeBytes.set -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.PrometheusAspNetCoreOptions() -> void
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ResourceConstantLabels.get -> System.Func<string!, bool>?
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ResourceConstantLabels.set -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ Notes](../../RELEASENOTES.md).
`null` (no resource attributes are added as metric labels).
([#7471](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7471))

* Add `PrometheusAspNetCoreOptions.MaxScrapeResponseSizeBytes` to configure
the maximum size of a scrape response. The default is now ~166 MiB.
([#7487](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7487))

* A scrape whose serialized output exceeds the maximum scrape response size
limit now responds with HTTP 500.
([#7487](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7487))

## 1.16.0-beta.1

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,17 @@ public Func<string, bool>? ResourceConstantLabels
set => this.ExporterOptions.ResourceConstantLabels = value;
}

/// <summary>
/// Gets or sets the maximum size in bytes that a single scrape response is allowed to grow to. Default value: ~166 MiB.
/// </summary>
/// <remarks>
/// Increase this value when exposing a very large number of time series.
/// </remarks>
public int MaxScrapeResponseSizeBytes
{
get => this.ExporterOptions.MaxScrapeResponseSizeBytes;
set => this.ExporterOptions.MaxScrapeResponseSizeBytes = value;
}

internal PrometheusExporterOptions ExporterOptions { get; } = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,29 @@ public async Task InvokeAsync(HttpContext httpContext)
{
linkedCts.Token.ThrowIfCancellationRequested();

var dataView = collectionResponse.View;

response.StatusCode = StatusCodes.Status200OK;

if (dataView.Count > 0)
if (!collectionResponse.Succeeded)
{
response.Headers.Append("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
response.ContentType = PrometheusProtocol.GetContentType(protocol);

await WriteResponseAsync(response, dataView.Array.AsMemory(0, dataView.Count), AcceptsGZip(requestHeaders), linkedCts.Token);
PrometheusExporterEventSource.Log.ScrapeFailed();
response.StatusCode = StatusCodes.Status500InternalServerError;
}
else
{
// It's not expected to have no metrics to collect, but it's not necessarily a failure, either.
PrometheusExporterEventSource.Log.NoMetrics();
var dataView = collectionResponse.View;

response.StatusCode = StatusCodes.Status200OK;

if (dataView.Count > 0)
{
response.Headers.Append("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
response.ContentType = PrometheusProtocol.GetContentType(protocol);

await WriteResponseAsync(response, dataView.Array.AsMemory(0, dataView.Count), AcceptsGZip(requestHeaders), linkedCts.Token);
}
else
{
// It's not expected to have no metrics to collect, but it's not necessarily a failure, either.
PrometheusExporterEventSource.Log.NoMetrics();
}
}
}
catch (OperationCanceledException ex) when (ex.CancellationToken == linkedCts.Token)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
OpenTelemetry.Exporter.PrometheusHttpListenerOptions
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ConfigureHttpListener.get -> System.Action<OpenTelemetry.Exporter.PrometheusHttpListenerOptions!, System.Net.HttpListener!>?
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ConfigureHttpListener.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.Host.get -> string!
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.Host.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.MaxScrapeResponseSizeBytes.get -> int
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.MaxScrapeResponseSizeBytes.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.Port.get -> int
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.Port.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.DisableTotalNameSuffixForCounters.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ResourceConstantLabels.get -> System.Func<string!, bool>?
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ResourceConstantLabels.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.get -> string?
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeEndpointPath.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeResponseCacheDurationMilliseconds.get -> int
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScrapeResponseCacheDurationMilliseconds.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.set -> void
OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions
static OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions.AddPrometheusHttpListener(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder!
static OpenTelemetry.Metrics.PrometheusHttpListenerMeterProviderBuilderExtensions.AddPrometheusHttpListener(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, string? name, System.Action<OpenTelemetry.Exporter.PrometheusHttpListenerOptions!>? configure) -> OpenTelemetry.Metrics.MeterProviderBuilder!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ Notes](../../RELEASENOTES.md).
`null` (no resource attributes are added as metric labels).
([#7471](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7471))

* Add `PrometheusHttpListenerOptions.MaxScrapeResponseSizeBytes` to configure
the maximum size of a scrape response. The default is now ~166 MiB.
([#7487](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7487))

* A scrape whose serialized output exceeds the maximum scrape response size
limit now responds with HTTP 500.
([#7487](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7487))

## 1.16.0-beta.1

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal sealed class PrometheusCollectionManager
private readonly long baseTimestamp = Stopwatch.GetTimestamp();
private readonly PrometheusExporter.ExportFunc onCollectRef;
private readonly Dictionary<Metric, PrometheusMetric> metricsCache;
private readonly int maxBufferSize;

private int metricsCacheCount;
private IReadOnlyList<KeyValuePair<string, object>>? resourceConstantLabels;
private bool resourceConstantLabelsComputed;
Expand All @@ -41,6 +43,7 @@ public PrometheusCollectionManager(PrometheusExporter exporter)
this.onCollectRef = this.OnCollect;
this.metricsCache = [];
this.GetElapsedTime = () => Stopwatch.GetElapsedTime(this.baseTimestamp);
this.maxBufferSize = this.exporter.MaxScrapeResponseSizeBytes;
}

internal Func<DateTime> UtcNow { get; set; } = static () => DateTime.UtcNow;
Expand Down Expand Up @@ -430,6 +433,14 @@ private CollectionResult CreateCollectionResult(CollectionContext collectionCont
var protocols = executionResult.Protocols ?? collectionContext.FreezeProtocols();
var responses = new Dictionary<PrometheusProtocol, CollectionResponse>(protocols.Length);

if (!succeeded && executionResult.Protocols is not null)
{
foreach (var protocol in protocols)
{
responses[protocol] = default;
}
}

if (succeeded)
{
var generatedAt = this.UtcNow();
Expand All @@ -441,6 +452,7 @@ private CollectionResult CreateCollectionResult(CollectionContext collectionCont
if (successfulProtocols is not null &&
!successfulProtocols.Contains(protocol))
{
responses[protocol] = default;
continue;
}

Expand Down Expand Up @@ -480,8 +492,12 @@ state.GeneratedAtElapsed is { } generatedAtElapsed &&
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private PrometheusProtocolState GetProtocolState(in PrometheusProtocol protocol)
=> this.protocolStates.GetOrAdd(protocol, static _ => new());
private PrometheusProtocolState GetProtocolState(in PrometheusProtocol protocol) =>
#if NET
this.protocolStates.GetOrAdd(protocol, static (_, maxBufferSize) => new(maxBufferSize), this.maxBufferSize);
#else
this.protocolStates.GetOrAdd(protocol, (_) => new(this.maxBufferSize));
#endif

private IReadOnlyList<KeyValuePair<string, object>>? GetResourceConstantLabels()
{
Expand Down Expand Up @@ -653,13 +669,23 @@ public CollectionResponse(ArraySegment<byte> view, DateTime generatedAtUtc, bool
this.View = view;
this.GeneratedAtUtc = generatedAtUtc;
this.FromCache = fromCache;
this.Succeeded = true;
}

public readonly ArraySegment<byte> View { get; }

public readonly DateTime GeneratedAtUtc { get; }

public readonly bool FromCache { get; }

/// <summary>
/// Gets a value indicating whether the collection that produced this response succeeded.
/// </summary>
/// <remarks>
/// Used to distinguish between a successful collection that produced an empty response
/// and a failed collection that produced no response.
/// </remarks>
public readonly bool Succeeded { get; }
}

private readonly struct CollectionResult
Expand Down Expand Up @@ -816,11 +842,16 @@ public bool TryRegisterProtocol(in PrometheusProtocol protocol, bool hasActiveRe

private sealed class PrometheusProtocolState
{
private const int InitialBufferSize = 85_000; // Encourage the object to live in Large Object Heap (LOH)
private const int MaxBufferSize = 100 * 1024 * 1024; // 100 MB
private const int InitialBufferSize = PrometheusExporterOptions.InitialScrapeResponseSizeBytes;
private readonly int maxBufferSize;

private int readerCount;

public PrometheusProtocolState(int maxBufferSize)
{
this.maxBufferSize = Math.Max(maxBufferSize, InitialBufferSize);
}

public static ArraySegment<byte> EmptyView { get; } =
#if NET
ArraySegment<byte>.Empty;
Expand All @@ -847,13 +878,17 @@ public void IncrementReaderCount()

public bool TryExpandBuffer()
{
var newBufferSize = this.Buffer.Length * 2;

if (newBufferSize > MaxBufferSize)
if (this.Buffer.Length >= this.maxBufferSize)
{
return false;
}

// Grow by doubling, but never past the configured maximum. Clamping
// to the maximum (rather than refusing the grow outright when the
// doubled size would overshoot) means the entire configured budget
// is usable, with no unreachable remainder.
var newBufferSize = (int)Math.Min((long)this.Buffer.Length * 2, this.maxBufferSize);

var expanded = new byte[newBufferSize];
this.Buffer.CopyTo(expanded, 0);
this.Buffer = expanded;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public PrometheusExporter(PrometheusExporterOptions options)
this.TargetInfoEnabled = options.TargetInfoEnabled;
this.DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters;
this.ResourceConstantLabels = options.ResourceConstantLabels;
this.MaxScrapeResponseSizeBytes = options.MaxScrapeResponseSizeBytes;

this.CollectionManager = new PrometheusCollectionManager(this);
}
Expand Down Expand Up @@ -55,6 +56,8 @@ public PrometheusExporter(PrometheusExporterOptions options)

internal Func<string, bool>? ResourceConstantLabels { get; }

internal int MaxScrapeResponseSizeBytes { get; }

internal Resource Resource
{
get => field ??= this.ParentProvider.GetResource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@ public void MetricIgnored(Metrics.Metric metric)
[Event(10, Message = "Metric '{0}' ignored as metrics of type '{1}' are not supported by Prometheus.", Level = EventLevel.Verbose)]
public void MetricIgnored(string metricName, string metricType)
=> this.WriteEvent(10, metricName, metricType);

[Event(11, Message = "Failed to collect metrics for a scrape request; the response may have exceeded the configured maximum size.", Level = EventLevel.Error)]
public void ScrapeFailed()
=> this.WriteEvent(11);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,31 @@ namespace OpenTelemetry.Exporter.Prometheus;
/// </summary>
internal sealed class PrometheusExporterOptions
{
/// <summary>
/// The initial scrape response buffer size in bytes.
/// The buffer is always allocated at this size, so it is also the
/// smallest meaningful value for <see cref="MaxScrapeResponseSizeBytes"/>.
/// </summary>
public const int InitialScrapeResponseSizeBytes = 85_000; // Encourage the object to live in the Large Object Heap (LOH).

/// <summary>
/// The default maximum scrape response size in bytes (~166 MiB).
/// </summary>
/// <remarks>
/// The response buffer starts at <see cref="InitialScrapeResponseSizeBytes"/>
/// and grows on demand by doubling. This default is equal to that size
/// multiplied by <c>2^11</c>, i.e. the largest size reachable by that doubling
/// sequence, so the whole budget is usable and none is left as an unreachable
/// remainder.
/// </remarks>
public const int DefaultMaxScrapeResponseSizeBytes = InitialScrapeResponseSizeBytes * 2048;

public PrometheusExporterOptions()
{
this.ScopeInfoEnabled = true;
this.ScrapeResponseCacheDurationMilliseconds = 300;
this.TargetInfoEnabled = true;
this.MaxScrapeResponseSizeBytes = DefaultMaxScrapeResponseSizeBytes;
}

/// <summary>
Expand Down Expand Up @@ -56,4 +76,21 @@ public int ScrapeResponseCacheDurationMilliseconds
/// attribute. Default value: <see langword="null"/> (no resource attributes are added as metric labels).
/// </summary>
public Func<string, bool>? ResourceConstantLabels { get; set; }

/// <summary>
/// Gets or sets the maximum size in bytes that a single scrape response is
/// allowed to grow to. Default value: <see cref="DefaultMaxScrapeResponseSizeBytes"/> (~166 MiB).
/// </summary>
/// <remarks>
/// Increase this value when exposing a very large number of time series.
/// </remarks>
public int MaxScrapeResponseSizeBytes
{
get;
set
{
Guard.ThrowIfOutOfRange(value, min: InitialScrapeResponseSizeBytes);
field = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,25 +262,34 @@ private async Task ProcessRequestAsync(HttpListenerContext context, Cancellation

context.Response.Headers.Add("Server", string.Empty);

var dataView = collectionResponse.View;

if (dataView.Count > 0)
if (!collectionResponse.Succeeded)
{
context.Response.StatusCode = 200;
context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
context.Response.ContentType = PrometheusProtocol.GetContentType(protocol);
PrometheusExporterEventSource.Log.ScrapeFailed();
context.Response.StatusCode = 500;
context.Response.ContentLength64 = 0;
}
else
{
var dataView = collectionResponse.View;

if (dataView.Count > 0)
{
context.Response.StatusCode = 200;
context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
context.Response.ContentType = PrometheusProtocol.GetContentType(protocol);

#if NET
await context.Response.OutputStream.WriteAsync(dataView.Array.AsMemory(0, dataView.Count), linkedCts.Token).ConfigureAwait(false);
await context.Response.OutputStream.WriteAsync(dataView.Array.AsMemory(0, dataView.Count), linkedCts.Token).ConfigureAwait(false);
#else
await context.Response.OutputStream.WriteAsync(dataView.Array, 0, dataView.Count, linkedCts.Token).ConfigureAwait(false);
await context.Response.OutputStream.WriteAsync(dataView.Array, 0, dataView.Count, linkedCts.Token).ConfigureAwait(false);
#endif
}
else
{
// It's not expected to have no metrics to collect, but it's not necessarily a failure, either.
context.Response.StatusCode = 200;
PrometheusExporterEventSource.Log.NoMetrics();
}
else
{
// It's not expected to have no metrics to collect, but it's not necessarily a failure, either.
context.Response.StatusCode = 200;
PrometheusExporterEventSource.Log.NoMetrics();
}
}
}
catch (OperationCanceledException ex) when (ex.CancellationToken == requestCancelled.Token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ private static BaseExportingMetricReader BuildPrometheusHttpListenerMetricReader
TargetInfoEnabled = options.TargetInfoEnabled,
DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters,
ResourceConstantLabels = options.ResourceConstantLabels,
MaxScrapeResponseSizeBytes = options.MaxScrapeResponseSizeBytes,
});

var reader = new BaseExportingMetricReader(exporter)
Expand Down
Loading