Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exemplar support to Prometheus exporter #5929

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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 @@ -13,6 +13,7 @@ Released 2024-Nov-12

* Added meter-level tags to Prometheus exporter
([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837))
* Added exemplar support to Prometheus exporter ([#5929](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5929))

* Updated OpenTelemetry core component version(s) to `1.10.0`.
([#5970](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5970))
Expand Down
6 changes: 2 additions & 4 deletions src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ to scrape.
<!-- This comment is to make sure the two notes above and below are not merged -->

> [!NOTE]
saul marked this conversation as resolved.
Show resolved Hide resolved
> This exporter does not support Exemplars. For using Exemplars, use the [OTLP
Exporter](../OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) and use a
component like OTel Collector to expose metrics (with exemplars) to Prometheus.
This [tutorial](../../docs/metrics/exemplars/README.md) shows one way how to do that.
> Exemplars are only exported when the OpenMetrics format is requested (e.g.
> with a `Accept: application/openmetrics-text` header).

## Prerequisite

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Released 2024-Nov-12

* Added meter-level tags to Prometheus exporter
([#5837](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5837))
* Added exemplar support to Prometheus exporter ([#5929](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5929))

* Updated OpenTelemetry core component version(s) to `1.10.0`.
([#5970](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5970))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,10 @@ public void NoMetrics()
{
this.WriteEvent(4);
}

[Event(5, Message = "Ignoring exemplar tags that are too long for metric: '{0}'", Level = EventLevel.Warning)]
public void ExemplarTagsTooLong(string metricName)
{
this.WriteEvent(5);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using OpenTelemetry.Metrics;

namespace OpenTelemetry.Exporter.Prometheus;
Expand Down Expand Up @@ -28,8 +29,11 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric, openMetricsRequested);
cursor = WriteHelpMetadata(buffer, cursor, prometheusMetric, metric.Description, openMetricsRequested);

var isLong = metric.MetricType.IsLong();
if (!metric.MetricType.IsHistogram())
{
var isSum = metric.MetricType.IsSum();

foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();
Expand All @@ -40,12 +44,9 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

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

// TODO: MetricType is same for all MetricPoints
// within a given Metric, so this check can avoided
// for each MetricPoint
if (((int)metric.MetricType & 0b_0000_1111) == 0x0a /* I8 */)
if (isLong)
{
if (metric.MetricType.IsSum())
if (isSum)
{
cursor = WriteLong(buffer, cursor, metricPoint.GetSumLong());
}
Expand All @@ -56,7 +57,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
}
else
{
if (metric.MetricType.IsSum())
if (isSum)
{
cursor = WriteDouble(buffer, cursor, metricPoint.GetSumDouble());
}
Expand All @@ -70,16 +71,27 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested);

if (isSum && openMetricsRequested && metricPoint.TryGetExemplars(out var exemplarCollection))
{
cursor = WriteSumExemplar(buffer, cursor, metric, exemplarCollection);
}

buffer[cursor++] = ASCII_LINEFEED;
}
}
else
{
Debug.Assert(!isLong, "Expected histogram metric to be of type `double`");

foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
var tags = metricPoint.Tags;
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();

metricPoint.TryGetExemplars(out var exemplarCollection);
saul marked this conversation as resolved.
Show resolved Hide resolved
var exemplars = exemplarCollection.GetEnumerator();
var hasExemplar = exemplars.MoveNext();

long totalCount = 0;
foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets())
{
Expand Down Expand Up @@ -107,6 +119,19 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested);

if (hasExemplar && openMetricsRequested)
{
if (exemplars.Current.DoubleValue <= histogramMeasurement.ExplicitBound)
{
cursor = WriteExemplar(buffer, cursor, exemplars.Current, metric.Name, isLong: false);
}

while (hasExemplar && exemplars.Current.DoubleValue <= histogramMeasurement.ExplicitBound)
{
hasExemplar = exemplars.MoveNext();
}
}

buffer[cursor++] = ASCII_LINEFEED;
}

Expand Down Expand Up @@ -142,4 +167,99 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

return cursor;
}

private static int WriteSumExemplar(
byte[] buffer,
int cursor,
in Metric metric,
in ReadOnlyExemplarCollection exemplarCollection)
{
var exemplars = exemplarCollection.GetEnumerator();
if (!exemplars.MoveNext())
{
return cursor;
}

ref readonly Exemplar maxExemplar = ref exemplars.Current;
var isLong = metric.MetricType.IsLong();

while (exemplars.MoveNext())
{
if (isLong)
{
if (exemplars.Current.LongValue >= maxExemplar.LongValue)
{
maxExemplar = ref exemplars.Current;
}
}
else
{
if (exemplars.Current.DoubleValue >= maxExemplar.DoubleValue)
{
maxExemplar = ref exemplars.Current;
}
}
}

return WriteExemplar(buffer, cursor, maxExemplar, metric.Name, isLong);
}

private static int WriteExemplar(byte[] buffer, int cursor, in Exemplar exemplar, string metricName, bool isLong)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this go into PrometheusSerializer.cs alongside the rest of the byte-writing code for metrics?

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

buffer[cursor++] = unchecked((byte)'{');
var labelSetCursorStart = cursor;
cursor = WriteAsciiStringNoEscape(buffer, cursor, "trace_id=\"");
cursor = WriteAsciiStringNoEscape(buffer, cursor, exemplar.TraceId.ToHexString());
cursor = WriteAsciiStringNoEscape(buffer, cursor, "\",span_id=\"");
cursor = WriteAsciiStringNoEscape(buffer, cursor, exemplar.SpanId.ToHexString());
buffer[cursor++] = unchecked((byte)'"');
buffer[cursor++] = unchecked((byte)',');

var labelSetWritten = cursor - labelSetCursorStart - 8;

var tagResetCursor = cursor;

foreach (var tag in exemplar.FilteredTags)
{
var prevCursor = cursor;
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);

// From the spec:
// Other characters in the text rendering of an exemplar such as ",= are not included in this limit
// for implementation simplicity and for consistency between the text and proto formats.
labelSetWritten += cursor - prevCursor - 3; // subtract 2 x " and 1 x = character

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

// From the spec:
// The combined length of the label names and values of an Exemplar's LabelSet MUST NOT exceed 128 UTF-8 character code points.
if (labelSetWritten > 128)
{
cursor = tagResetCursor;
PrometheusExporterEventSource.Log.ExemplarTagsTooLong(metricName);
break;
}
}

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

if (isLong)
{
cursor = WriteLong(buffer, cursor, exemplar.LongValue);
}
else
{
cursor = WriteDouble(buffer, cursor, exemplar.DoubleValue);
}

buffer[cursor++] = unchecked((byte)' ');
cursor = WriteTimestamp(buffer, cursor, exemplar.Timestamp.ToUnixTimeMilliseconds(), useOpenMetrics: true);

return cursor;
}
}
Loading
Loading