Skip to content

Commit 8550c31

Browse files
committed
Add exemplar support to Prometheus exporter
1 parent 84ff215 commit 8550c31

File tree

7 files changed

+1127
-68
lines changed

7 files changed

+1127
-68
lines changed

src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Notes](../../RELEASENOTES.md).
99

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

1314
## 1.9.0-beta.2
1415

src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@ to scrape.
1818
Grafana](../../docs/metrics/getting-started-prometheus-grafana/README.md)
1919
tutorial for more information.
2020

21-
<!-- This comment is to make sure the two notes above and below are not merged -->
22-
23-
> [!NOTE]
24-
> This exporter does not support Exemplars. For using Exemplars, use the [OTLP
25-
Exporter](../OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) and use a
26-
component like OTel Collector to expose metrics (with exemplars) to Prometheus.
27-
This [tutorial](../../docs/metrics/exemplars/README.md) shows one way how to do that.
28-
2921
## Prerequisite
3022

3123
* [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/)

src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Notes](../../RELEASENOTES.md).
99

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

1314
## 1.9.0-beta.2
1415

src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusExporterEventSource.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,10 @@ public void NoMetrics()
6464
{
6565
this.WriteEvent(4);
6666
}
67+
68+
[Event(5, Message = "Ignoring exemplar tags that are too long for metric: '{0}'", Level = EventLevel.Warning)]
69+
public void ExemplarTagsTooLong(string metricName)
70+
{
71+
this.WriteEvent(5);
72+
}
6773
}

src/OpenTelemetry.Exporter.Prometheus.HttpListener/Internal/PrometheusSerializerExt.cs

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright The OpenTelemetry Authors
22
// SPDX-License-Identifier: Apache-2.0
33

4+
using System.Diagnostics;
45
using OpenTelemetry.Metrics;
56

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

32+
var isLong = metric.MetricType.IsLong();
3133
if (!metric.MetricType.IsHistogram())
3234
{
35+
var isSum = metric.MetricType.IsSum();
36+
3337
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
3438
{
3539
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();
@@ -40,12 +44,9 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
4044

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

43-
// TODO: MetricType is same for all MetricPoints
44-
// within a given Metric, so this check can avoided
45-
// for each MetricPoint
46-
if (((int)metric.MetricType & 0b_0000_1111) == 0x0a /* I8 */)
47+
if (isLong)
4748
{
48-
if (metric.MetricType.IsSum())
49+
if (isSum)
4950
{
5051
cursor = WriteLong(buffer, cursor, metricPoint.GetSumLong());
5152
}
@@ -56,7 +57,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
5657
}
5758
else
5859
{
59-
if (metric.MetricType.IsSum())
60+
if (isSum)
6061
{
6162
cursor = WriteDouble(buffer, cursor, metricPoint.GetSumDouble());
6263
}
@@ -70,16 +71,27 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
7071

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

74+
if (isSum && openMetricsRequested && metricPoint.TryGetExemplars(out var exemplarCollection))
75+
{
76+
cursor = WriteSumExemplar(buffer, cursor, metric, exemplarCollection);
77+
}
78+
7379
buffer[cursor++] = ASCII_LINEFEED;
7480
}
7581
}
7682
else
7783
{
84+
Debug.Assert(!isLong, "Expected histogram metric to be of type `double`");
85+
7886
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
7987
{
8088
var tags = metricPoint.Tags;
8189
var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds();
8290

91+
metricPoint.TryGetExemplars(out var exemplarCollection);
92+
var exemplars = exemplarCollection.GetEnumerator();
93+
var hasExemplar = exemplars.MoveNext();
94+
8395
long totalCount = 0;
8496
foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets())
8597
{
@@ -107,6 +119,19 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
107119

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

122+
if (hasExemplar && openMetricsRequested)
123+
{
124+
if (exemplars.Current.DoubleValue <= histogramMeasurement.ExplicitBound)
125+
{
126+
cursor = WriteExemplar(buffer, cursor, exemplars.Current, metric.Name, isLong: false);
127+
}
128+
129+
while (hasExemplar && exemplars.Current.DoubleValue <= histogramMeasurement.ExplicitBound)
130+
{
131+
hasExemplar = exemplars.MoveNext();
132+
}
133+
}
134+
110135
buffer[cursor++] = ASCII_LINEFEED;
111136
}
112137

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

143168
return cursor;
144169
}
170+
171+
private static int WriteSumExemplar(
172+
byte[] buffer,
173+
int cursor,
174+
in Metric metric,
175+
in ReadOnlyExemplarCollection exemplarCollection)
176+
{
177+
var exemplars = exemplarCollection.GetEnumerator();
178+
if (!exemplars.MoveNext())
179+
{
180+
return cursor;
181+
}
182+
183+
ref readonly Exemplar maxExemplar = ref exemplars.Current;
184+
var isLong = metric.MetricType.IsLong();
185+
186+
while (exemplars.MoveNext())
187+
{
188+
if (isLong)
189+
{
190+
if (exemplars.Current.LongValue >= maxExemplar.LongValue)
191+
{
192+
maxExemplar = ref exemplars.Current;
193+
}
194+
}
195+
else
196+
{
197+
if (exemplars.Current.DoubleValue >= maxExemplar.DoubleValue)
198+
{
199+
maxExemplar = ref exemplars.Current;
200+
}
201+
}
202+
}
203+
204+
return WriteExemplar(buffer, cursor, maxExemplar, metric.Name, isLong);
205+
}
206+
207+
private static int WriteExemplar(byte[] buffer, int cursor, in Exemplar exemplar, string metricName, bool isLong)
208+
{
209+
buffer[cursor++] = unchecked((byte)' ');
210+
buffer[cursor++] = unchecked((byte)'#');
211+
buffer[cursor++] = unchecked((byte)' ');
212+
213+
buffer[cursor++] = unchecked((byte)'{');
214+
var labelSetCursorStart = cursor;
215+
cursor = WriteAsciiStringNoEscape(buffer, cursor, "trace_id=\"");
216+
cursor = WriteAsciiStringNoEscape(buffer, cursor, exemplar.TraceId.ToHexString());
217+
cursor = WriteAsciiStringNoEscape(buffer, cursor, "\",span_id=\"");
218+
cursor = WriteAsciiStringNoEscape(buffer, cursor, exemplar.SpanId.ToHexString());
219+
buffer[cursor++] = unchecked((byte)'"');
220+
buffer[cursor++] = unchecked((byte)',');
221+
222+
var labelSetWritten = cursor - labelSetCursorStart - 8;
223+
224+
var tagResetCursor = cursor;
225+
226+
foreach (var tag in exemplar.FilteredTags)
227+
{
228+
var prevCursor = cursor;
229+
cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value);
230+
231+
// From the spec:
232+
// Other characters in the text rendering of an exemplar such as ",= are not included in this limit
233+
// for implementation simplicity and for consistency between the text and proto formats.
234+
labelSetWritten += cursor - prevCursor - 3; // subtract 2 x " and 1 x = character
235+
236+
buffer[cursor++] = unchecked((byte)',');
237+
238+
// From the spec:
239+
// The combined length of the label names and values of an Exemplar's LabelSet MUST NOT exceed 128 UTF-8 character code points.
240+
if (labelSetWritten > 128)
241+
{
242+
cursor = tagResetCursor;
243+
PrometheusExporterEventSource.Log.ExemplarTagsTooLong(metricName);
244+
break;
245+
}
246+
}
247+
248+
buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra.
249+
buffer[cursor++] = unchecked((byte)' ');
250+
251+
if (isLong)
252+
{
253+
cursor = WriteLong(buffer, cursor, exemplar.LongValue);
254+
}
255+
else
256+
{
257+
cursor = WriteDouble(buffer, cursor, exemplar.DoubleValue);
258+
}
259+
260+
buffer[cursor++] = unchecked((byte)' ');
261+
cursor = WriteTimestamp(buffer, cursor, exemplar.Timestamp.ToUnixTimeMilliseconds(), useOpenMetrics: true);
262+
263+
return cursor;
264+
}
145265
}

0 commit comments

Comments
 (0)