Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.ScopeInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusAspNetCoreOptions.ScopeInfoEnabled.set -> void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ Notes](../../RELEASENOTES.md).
when negotiated via the `Accept` header.
([#7440](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7440))

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

* A scrape whose serialized output exceeds the maximum scrape response size
limit now responds with HTTP 500.
([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/XXXX))
Comment thread
martincostello marked this conversation as resolved.
Outdated

## 1.16.0-beta.1

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,17 @@ public bool TargetInfoEnabled
set => this.ExporterOptions.TargetInfoEnabled = 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,21 +1,23 @@
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.MaxScrapeResponseSizeBytes.get -> int
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.MaxScrapeResponseSizeBytes.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.Host.get -> string!
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.Host.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.PrometheusHttpListenerOptions() -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.ScopeInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.get -> bool
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.TargetInfoEnabled.set -> void
OpenTelemetry.Exporter.PrometheusHttpListenerOptions.PrometheusHttpListenerOptions() -> 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 @@ -41,6 +41,14 @@ Notes](../../RELEASENOTES.md).
when negotiated via the `Accept` header.
([#7440](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7440))

* Add `PrometheusHttpListenerOptions.MaxScrapeResponseSizeBytes` to configure
the maximum size of a scrape response. The default is now ~166 MiB.
([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO))
Comment thread
martincostello marked this conversation as resolved.
Outdated

* A scrape whose serialized output exceeds the maximum scrape response size
limit now responds with HTTP 500.
([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO))
Comment thread
martincostello marked this conversation as resolved.
Outdated

## 1.16.0-beta.1

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 int globalLockState;
private CollectionContext? collectionContext;
Expand All @@ -37,6 +39,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 @@ -474,8 +477,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 int WriteTargetInfo(TextFormatSerializer serializer, PrometheusProtocolState state)
{
Expand Down Expand Up @@ -621,13 +628,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 @@ -784,11 +801,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 @@ -815,13 +837,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 @@ -27,6 +27,7 @@ public PrometheusExporter(PrometheusExporterOptions options)
this.ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds;
this.TargetInfoEnabled = options.TargetInfoEnabled;
this.DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters;
this.MaxScrapeResponseSizeBytes = options.MaxScrapeResponseSizeBytes;

this.CollectionManager = new PrometheusCollectionManager(this);
}
Expand All @@ -52,6 +53,8 @@ public PrometheusExporter(PrometheusExporterOptions options)

internal bool DisableTotalNameSuffixForCounters { 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 @@ -49,4 +69,21 @@ public int ScrapeResponseCacheDurationMilliseconds
/// Gets or sets a value indicating whether addition of _total suffix for counter metric names is disabled. Default value: <see langword="false"/>.
/// </summary>
public bool DisableTotalNameSuffixForCounters { 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 @@ -71,6 +71,7 @@ private static BaseExportingMetricReader BuildPrometheusHttpListenerMetricReader
ScrapeResponseCacheDurationMilliseconds = options.ScrapeResponseCacheDurationMilliseconds,
TargetInfoEnabled = options.TargetInfoEnabled,
DisableTotalNameSuffixForCounters = options.DisableTotalNameSuffixForCounters,
MaxScrapeResponseSizeBytes = options.MaxScrapeResponseSizeBytes,
});

var reader = new BaseExportingMetricReader(exporter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ internal PrometheusHttpListenerOptions(IConfiguration configuration)
this.Host = host;
this.Port = port;
this.ScrapeResponseCacheDurationMilliseconds = 300;
this.MaxScrapeResponseSizeBytes = PrometheusExporterOptions.DefaultMaxScrapeResponseSizeBytes;
}

/// <summary>
Expand Down Expand Up @@ -99,6 +100,22 @@ public int ScrapeResponseCacheDurationMilliseconds
/// </summary>
public bool TargetInfoEnabled { get; set; } = true;

/// <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;
set
{
Guard.ThrowIfOutOfRange(value, min: PrometheusExporterOptions.InitialScrapeResponseSizeBytes);
field = value;
}
}

/// <summary>
/// Gets or sets an optional callback to apply custom configuration for the
/// <see cref="System.Net.HttpListener"/> instance used by the exporter.
Expand Down
Loading