11// Copyright The OpenTelemetry Authors
22// SPDX-License-Identifier: Apache-2.0
33
4+ using System . Diagnostics ;
45using OpenTelemetry . Metrics ;
56
67namespace 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