Skip to content
Open
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 @@ -66,6 +66,9 @@ Notes](../../RELEASENOTES.md).
`X-Prometheus-Scrape-Timeout-Seconds` HTTP request header.
([#7252](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7252))

* Add Prometheus text fallback `target_info` output as a gauge.
([#7238](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7238))

## 1.15.3-beta.1

Released 2026-Apr-21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ Notes](../../RELEASENOTES.md).
`X-Prometheus-Scrape-Timeout-Seconds` HTTP request header.
([#7252](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7252))

* Add Prometheus text fallback `target_info` output as a gauge.
([#7238](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7238))

## 1.15.3-beta.1

Released 2026-Apr-21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ internal sealed class PrometheusCollectionManager
private int metricsCacheCount;
private byte[] plainTextBuffer = new byte[85000]; // encourage the object to live in LOH (large object heap)
private byte[] openMetricsBuffer = new byte[85000]; // encourage the object to live in LOH (large object heap)
private int targetInfoBufferLength = -1; // zero or positive when target_info has been written for the first time
private int plainTextTargetInfoBufferLength = -1;
private int openMetricsTargetInfoBufferLength = -1;
private ArraySegment<byte> previousPlainTextDataView;
private ArraySegment<byte> previousOpenMetricsDataView;
private int globalLockState;
Expand Down Expand Up @@ -237,10 +238,10 @@ private ExportResult OnCollect(in Batch<Metric> metrics)

try
{
cursor = this.WriteTargetInfo(ref buffer);

if (this.exporter.OpenMetricsRequested)
{
cursor = this.WriteTargetInfo(ref buffer);

this.scopes.Clear();

foreach (var metric in metrics)
Expand Down Expand Up @@ -356,14 +357,17 @@ private ExportResult OnCollect(in Batch<Metric> metrics)

private int WriteTargetInfo(ref byte[] buffer)
{
if (this.targetInfoBufferLength < 0)
ref var targetInfoBufferLength = ref this.exporter.OpenMetricsRequested
Comment thread
martincostello marked this conversation as resolved.
? ref this.openMetricsTargetInfoBufferLength
: ref this.plainTextTargetInfoBufferLength;

if (targetInfoBufferLength < 0)
{
while (true)
{
try
{
this.targetInfoBufferLength = PrometheusSerializer.WriteTargetInfo(buffer, 0, this.exporter.Resource, openMetricsRequested: true);

targetInfoBufferLength = PrometheusSerializer.WriteTargetInfo(buffer, 0, this.exporter.Resource, this.exporter.OpenMetricsRequested);
break;
}
catch (IndexOutOfRangeException)
Expand All @@ -376,7 +380,7 @@ private int WriteTargetInfo(ref byte[] buffer)
}
}

return this.targetInfoBufferLength;
return targetInfoBufferLength;
}

private PrometheusMetric GetPrometheusMetric(Metric metric)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,21 +500,37 @@ public static int WriteTargetInfo(byte[] buffer, int cursor, Resource resource,
return cursor;
}

cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE target info");
using var attributes = resource.Attributes.GetEnumerator();
if (!attributes.MoveNext())
{
return cursor;
}

// "If info-typed metric families are not yet supported...a gauge-typed metric
// family named target_info with a constant value of 1 MUST be used instead.".
// See https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE ");
cursor = WriteAsciiStringNoEscape(buffer, cursor, openMetricsRequested ? "target" : "target_info");
buffer[cursor++] = unchecked((byte)' ');
cursor = WriteAsciiStringNoEscape(buffer, cursor, openMetricsRequested ? "info" : "gauge");
buffer[cursor++] = ASCII_LINEFEED;

cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP target Target metadata");
cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP ");
cursor = WriteAsciiStringNoEscape(buffer, cursor, openMetricsRequested ? "target" : "target_info");
cursor = WriteAsciiStringNoEscape(buffer, cursor, " Target metadata");
buffer[cursor++] = ASCII_LINEFEED;

cursor = WriteAsciiStringNoEscape(buffer, cursor, "target_info");
buffer[cursor++] = unchecked((byte)'{');

foreach (var attribute in resource.Attributes)
do
{
var attribute = attributes.Current;
cursor = WriteLabel(buffer, cursor, attribute.Key, attribute.Value, openMetricsRequested);

buffer[cursor++] = unchecked((byte)',');
}
while (attributes.MoveNext());

cursor--; // Write over the last written comma

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,9 @@ private static async Task VerifyAsync(

""".ReplaceLineEndings()
: $$"""
# TYPE target_info gauge
# HELP target_info Target metadata
target_info{service_name="my_service",service_instance_id="id1"} 1
# TYPE counter_double_bytes_total counter
# UNIT counter_double_bytes_total bytes
counter_double_bytes_total{otel_scope_name="{{MeterName}}",otel_scope_version="{{MeterVersion}}",{{additionalTags}}key1="value1",key2="value2"} 101.17
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,34 +99,38 @@ public async Task Prometheus_Can_Scrape_Metrics(string scrapeProtocol) => await

await WaitForServiceDiscoveryAsync(prometheusBaseAddress, outputHelper, cts.Token);

IReadOnlyList<string> series = [];
HashSet<string> expectedSeries =
[
#if NET10_0_OR_GREATER
"aspnetcore_memory_pool_allocated_bytes_total",
#endif
"http_server_active_requests",
"http_server_request_duration_seconds_bucket",
"http_server_request_duration_seconds_count",
"http_server_request_duration_seconds_sum",
"kestrel_active_connections",
"kestrel_connection_duration_seconds_bucket",
"kestrel_connection_duration_seconds_count",
"kestrel_connection_duration_seconds_sum",
"processed_bytes_total",
"queue_balance",
"temperature_celsius",
];

HashSet<string> actualSeries = [];

// Assert
while (!cts.IsCancellationRequested)
{
series = await WaitForMetricsSeriesAsync(prometheusBaseAddress, outputHelper, cts.Token);
actualSeries = await WaitForMetricsSeriesAsync(prometheusBaseAddress, outputHelper, cts.Token);

if (series.Contains("temperature_celsius"))
if (actualSeries.IsProperSupersetOf(expectedSeries))
{
break;
}
}

Assert.Contains("http_server_active_requests", series);
Assert.Contains("http_server_request_duration_seconds_bucket", series);
Assert.Contains("http_server_request_duration_seconds_count", series);
Assert.Contains("http_server_request_duration_seconds_sum", series);
Assert.Contains("kestrel_active_connections", series);
Assert.Contains("kestrel_connection_duration_seconds_bucket", series);
Assert.Contains("kestrel_connection_duration_seconds_count", series);
Assert.Contains("kestrel_connection_duration_seconds_sum", series);
Assert.Contains("processed_bytes_total", series);
Assert.Contains("queue_balance", series);
Assert.Contains("temperature_celsius", series);

#if NET10_0_OR_GREATER
Assert.Contains("aspnetcore_memory_pool_allocated_bytes_total", series);
#endif
Assert.ProperSuperset(expectedSeries, actualSeries);
}
finally
{
Expand All @@ -138,7 +142,7 @@ public async Task Prometheus_Can_Scrape_Metrics(string scrapeProtocol) => await
await prometheus.DisposeAsync();
}

static async Task<IReadOnlyList<string>> WaitForMetricsSeriesAsync(
static async Task<HashSet<string>> WaitForMetricsSeriesAsync(
Uri baseAddress,
ITestOutputHelper outputHelper,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -182,7 +186,7 @@ static async Task<IReadOnlyList<string>> WaitForMetricsSeriesAsync(
}
}

return [.. series];
return series;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,10 @@ private static async Task RunPrometheusExporterHttpServerIntegrationTest(
+ "# UNIT counter_double_bytes bytes\n"
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17\n"
+ "# EOF\n"
: "# TYPE counter_double_bytes_total counter\n"
: "# TYPE target_info gauge\n"
+ "# HELP target_info Target metadata\n"
+ "target_info{service_name='my_service',service_instance_id='id1'} 1\n"
+ "# TYPE counter_double_bytes_total counter\n"
+ "# UNIT counter_double_bytes_total bytes\n"
+ $"counter_double_bytes_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',{additionalTags}key1='value1',key2='value2'}} 101.17\n"
+ "# EOF\n";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Globalization;
using System.Text;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Tests;
using Xunit;

Expand Down Expand Up @@ -1141,6 +1142,19 @@ public void WriteDoubleFormatsNaN()
Assert.Equal("NaN", Encoding.UTF8.GetString(buffer, 0, cursor));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void WriteTargetInfoSkipsEmptyNonSingletonResource(bool openMetricsRequested)
{
var buffer = new byte[128];
var resource = Resource.Empty.Merge(Resource.Empty);

var cursor = PrometheusSerializer.WriteTargetInfo(buffer, 0, resource, openMetricsRequested);

Assert.Equal(0, cursor);
}

[Fact]
public void WriteUnicodeStringEncodesSurrogatePairsAsUtf8ScalarValues()
{
Expand Down
Loading