Skip to content

Commit

Permalink
Add scope info and version to Prometheus exporter (#5086)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertcoltheart authored Dec 15, 2023
1 parent 2961ff5 commit 3c986e8
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 186 deletions.
2 changes: 1 addition & 1 deletion examples/AspNetCore/Instrumentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public Instrumentation()
string? version = typeof(Instrumentation).Assembly.GetName().Version?.ToString();
this.ActivitySource = new ActivitySource(ActivitySourceName, version);
this.meter = new Meter(MeterName, version);
this.FreezingDaysCounter = this.meter.CreateCounter<long>("weather.days.freezing", "The number of days where the temperature is below freezing");
this.FreezingDaysCounter = this.meter.CreateCounter<long>("weather.days.freezing", description: "The number of days where the temperature is below freezing");
}

public ActivitySource ActivitySource { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107))
* For requests with OpenMetrics format, scope info is automatically added ([#5086](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5086))

## 1.7.0-rc.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107))
* For requests with OpenMetrics format, scope info is automatically added ([#5086](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5086))

## 1.7.0-rc.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal sealed class PrometheusCollectionManager
private readonly int scrapeResponseCacheDurationMilliseconds;
private readonly Func<Batch<Metric>, ExportResult> onCollectRef;
private readonly Dictionary<Metric, PrometheusMetric> metricsCache;
private readonly HashSet<string> scopes;
private int metricsCacheCount;
private byte[] buffer = new byte[85000]; // encourage the object to live in LOH (large object heap)
private int globalLockState;
Expand All @@ -29,6 +30,7 @@ public PrometheusCollectionManager(PrometheusExporter exporter)
this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds;
this.onCollectRef = this.OnCollect;
this.metricsCache = new Dictionary<Metric, PrometheusMetric>();
this.scopes = new HashSet<string>();
}

#if NET6_0_OR_GREATER
Expand Down Expand Up @@ -170,6 +172,37 @@ private ExportResult OnCollect(Batch<Metric> metrics)

try
{
this.scopes.Clear();

foreach (var metric in metrics)
{
if (PrometheusSerializer.CanWriteMetric(metric))
{
if (this.scopes.Add(metric.MeterName))
{
try
{
cursor = PrometheusSerializer.WriteScopeInfo(this.buffer, cursor, metric.MeterName);

break;
}
catch (IndexOutOfRangeException)
{
if (!this.IncreaseBufferSize())
{
// there are two cases we might run into the following condition:
// 1. we have many metrics to be exported - in this case we probably want
// to put some upper limit and allow the user to configure it.
// 2. we got an IndexOutOfRangeException which was triggered by some other
// code instead of the buffer[cursor++] - in this case we should give up
// at certain point rather than allocating like crazy.
throw;
}
}
}
}
}

foreach (var metric in metrics)
{
if (!PrometheusSerializer.CanWriteMetric(metric))
Expand All @@ -194,12 +227,6 @@ private ExportResult OnCollect(Batch<Metric> metrics)
{
if (!this.IncreaseBufferSize())
{
// there are two cases we might run into the following condition:
// 1. we have many metrics to be exported - in this case we probably want
// to put some upper limit and allow the user to configure it.
// 2. we got an IndexOutOfRangeException which was triggered by some other
// code instead of the buffer[cursor++] - in this case we should give up
// at certain point rather than allocating like crazy.
throw;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using OpenTelemetry.Internal;
using OpenTelemetry.Metrics;

namespace OpenTelemetry.Exporter.Prometheus;

Expand Down Expand Up @@ -313,6 +314,31 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric
return cursor;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteScopeInfo(byte[] buffer, int cursor, string scopeName)
{
if (string.IsNullOrEmpty(scopeName))
{
return cursor;
}

cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE otel_scope_info info");
buffer[cursor++] = ASCII_LINEFEED;

cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP otel_scope_info Scope metadata");
buffer[cursor++] = ASCII_LINEFEED;

cursor = WriteAsciiStringNoEscape(buffer, cursor, "otel_scope_info");
buffer[cursor++] = unchecked((byte)'{');
cursor = WriteLabel(buffer, cursor, "otel_scope_name", scopeName);
buffer[cursor++] = unchecked((byte)'}');
buffer[cursor++] = unchecked((byte)' ');
buffer[cursor++] = unchecked((byte)'1');
buffer[cursor++] = ASCII_LINEFEED;

return cursor;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool useOpenMetrics)
{
Expand All @@ -339,6 +365,37 @@ public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool use
return WriteLong(buffer, cursor, value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteTags(byte[] buffer, int cursor, Metric metric, ReadOnlyTagCollection tags, bool writeEnclosingBraces = true)
{
if (writeEnclosingBraces)
{
buffer[cursor++] = unchecked((byte)'{');
}

cursor = WriteLabel(buffer, cursor, "otel_scope_name", metric.MeterName);
buffer[cursor++] = unchecked((byte)',');

if (!string.IsNullOrEmpty(metric.MeterVersion))
{
cursor = WriteLabel(buffer, cursor, "otel_scope_version", metric.MeterVersion);
buffer[cursor++] = unchecked((byte)',');
}

foreach (var tag in tags)
{
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
buffer[cursor++] = unchecked((byte)',');
}

if (writeEnclosingBraces)
{
buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra.
}

return cursor;
}

private static string MapPrometheusType(PrometheusType type)
{
return type switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,11 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
{
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
var tags = metricPoint.Tags;
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();

// Counter and Gauge
cursor = WriteMetricName(buffer, cursor, prometheusMetric);

if (tags.Count > 0)
{
buffer[cursor++] = unchecked((byte)'{');

foreach (var tag in tags)
{
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
buffer[cursor++] = unchecked((byte)',');
}

buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra.
}
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);

buffer[cursor++] = unchecked((byte)' ');

Expand Down Expand Up @@ -100,12 +87,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{");

foreach (var tag in tags)
{
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
buffer[cursor++] = unchecked((byte)',');
}
cursor = WriteTags(buffer, cursor, metric, tags, writeEnclosingBraces: false);

cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\"");

Expand All @@ -131,19 +113,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
// Histogram sum
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum");

if (tags.Count > 0)
{
buffer[cursor++] = unchecked((byte)'{');

foreach (var tag in tags)
{
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
buffer[cursor++] = unchecked((byte)',');
}

buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra.
}
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);

buffer[cursor++] = unchecked((byte)' ');

Expand All @@ -157,19 +127,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
// Histogram count
cursor = WriteMetricName(buffer, cursor, prometheusMetric);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count");

if (tags.Count > 0)
{
buffer[cursor++] = unchecked((byte)'{');

foreach (var tag in tags)
{
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
buffer[cursor++] = unchecked((byte)',');
}

buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra.
}
cursor = WriteTags(buffer, cursor, metric, metricPoint.Tags);

buffer[cursor++] = unchecked((byte)' ');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ namespace OpenTelemetry.Exporter.Prometheus.AspNetCore.Tests;

public sealed class PrometheusExporterMiddlewareTests
{
private const string MeterVersion = "1.0.1";

private static readonly string MeterName = Utils.GetCurrentMethodName();

[Fact]
Expand Down Expand Up @@ -281,7 +283,7 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
new KeyValuePair<string, object>("key2", "value2"),
};

using var meter = new Meter(MeterName);
using var meter = new Meter(MeterName, MeterVersion);

var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();

Expand Down Expand Up @@ -320,11 +322,17 @@ private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
string content = await response.Content.ReadAsStringAsync();

string expected = requestOpenMetrics
? "# TYPE counter_double_total counter\n"
+ "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+\\.\\d{3})\n"
? "# TYPE otel_scope_info info\n"
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ "# EOF\n"
: "# TYPE counter_double_total counter\n"
+ "counter_double_total{key1='value1',key2='value2'} 101.17 (\\d+)\n"
: "# TYPE otel_scope_info info\n"
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ "# EOF\n";

var matches = Regex.Matches(content, ("^" + expected + "$").Replace('\'', '"'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests;

public class PrometheusHttpListenerTests
{
private readonly string meterName = Utils.GetCurrentMethodName();
private const string MeterVersion = "1.0.1";

private static readonly string MeterName = Utils.GetCurrentMethodName();

[Theory]
[InlineData("http://+:9464")]
Expand Down Expand Up @@ -99,7 +101,7 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
string address = null;

MeterProvider provider = null;
using var meter = new Meter(this.meterName);
using var meter = new Meter(MeterName, MeterVersion);

while (retryAttempts-- != 0)
{
Expand All @@ -110,7 +112,10 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
{
provider = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddPrometheusHttpListener(options => options.UriPrefixes = new string[] { address })
.AddPrometheusHttpListener(options =>
{
options.UriPrefixes = new string[] { address };
})
.Build();

break;
Expand Down Expand Up @@ -165,11 +170,17 @@ private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetri
var content = await response.Content.ReadAsStringAsync();

var expected = requestOpenMetrics
? "# TYPE counter_double_total counter\n"
+ "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\\.\\d{3}\n"
? "# TYPE otel_scope_info info\n"
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+\\.\\d{{3}})\n"
+ "# EOF\n"
: "# TYPE counter_double_total counter\n"
+ "counter_double_total{key1='value1',key2='value2'} 101.17 \\d+\n"
: "# TYPE otel_scope_info info\n"
+ "# HELP otel_scope_info Scope metadata\n"
+ $"otel_scope_info{{otel_scope_name='{MeterName}'}} 1\n"
+ "# TYPE counter_double_total counter\n"
+ $"counter_double_total{{otel_scope_name='{MeterName}',otel_scope_version='{MeterVersion}',key1='value1',key2='value2'}} 101.17 (\\d+)\n"
+ "# EOF\n";

Assert.Matches(("^" + expected + "$").Replace('\'', '"'), content);
Expand Down
Loading

0 comments on commit 3c986e8

Please sign in to comment.