Skip to content

Commit 7419d85

Browse files
Export OpenMetrics format for prometheus exporters (#5107)
1 parent 4cf2bf4 commit 7419d85

File tree

15 files changed

+395
-46
lines changed

15 files changed

+395
-46
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107))
6+
57
## 1.7.0-rc.1
68

79
Released 2023-Nov-29

src/OpenTelemetry.Exporter.Prometheus.AspNetCore/OpenTelemetry.Exporter.Prometheus.AspNetCore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusSerializerExt.cs" Link="Includes/PrometheusSerializerExt.cs" />
2929
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusType.cs" Link="Includes/PrometheusType.cs" />
3030
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusMetric.cs" Link="Includes/PrometheusMetric.cs" />
31+
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus.HttpListener\Internal\PrometheusHeadersParser.cs" Link="Includes/PrometheusHeadersParser.cs" />
3132
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
3233
</ItemGroup>
3334

src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExporterMiddleware.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
using System.Diagnostics;
1818
using Microsoft.AspNetCore.Http;
19+
using Microsoft.Extensions.Primitives;
1920
using OpenTelemetry.Exporter.Prometheus;
2021
using OpenTelemetry.Internal;
2122
using OpenTelemetry.Metrics;
@@ -64,7 +65,9 @@ public async Task InvokeAsync(HttpContext httpContext)
6465

6566
try
6667
{
67-
var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false);
68+
var openMetricsRequested = AcceptsOpenMetrics(httpContext.Request);
69+
var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false);
70+
6871
try
6972
{
7073
if (collectionResponse.View.Count > 0)
@@ -75,7 +78,9 @@ public async Task InvokeAsync(HttpContext httpContext)
7578
#else
7679
response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
7780
#endif
78-
response.ContentType = "text/plain; charset=utf-8; version=0.0.4";
81+
response.ContentType = openMetricsRequested
82+
? "application/openmetrics-text; version=1.0.0; charset=utf-8"
83+
: "text/plain; charset=utf-8; version=0.0.4";
7984

8085
await response.Body.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false);
8186
}
@@ -102,4 +107,24 @@ public async Task InvokeAsync(HttpContext httpContext)
102107

103108
this.exporter.OnExport = null;
104109
}
110+
111+
private static bool AcceptsOpenMetrics(HttpRequest request)
112+
{
113+
var acceptHeader = request.Headers.Accept;
114+
115+
if (StringValues.IsNullOrEmpty(acceptHeader))
116+
{
117+
return false;
118+
}
119+
120+
foreach (var header in acceptHeader)
121+
{
122+
if (PrometheusHeadersParser.AcceptsOpenMetrics(header))
123+
{
124+
return true;
125+
}
126+
}
127+
128+
return false;
129+
}
105130
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107))
6+
57
## 1.7.0-rc.1
68

79
Released 2023-Nov-29

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public PrometheusCollectionManager(PrometheusExporter exporter)
4545
}
4646

4747
#if NET6_0_OR_GREATER
48-
public ValueTask<CollectionResponse> EnterCollect()
48+
public ValueTask<CollectionResponse> EnterCollect(bool openMetricsRequested)
4949
#else
50-
public Task<CollectionResponse> EnterCollect()
50+
public Task<CollectionResponse> EnterCollect(bool openMetricsRequested)
5151
#endif
5252
{
5353
this.EnterGlobalLock();
@@ -93,7 +93,7 @@ public Task<CollectionResponse> EnterCollect()
9393
this.ExitGlobalLock();
9494

9595
CollectionResponse response;
96-
var result = this.ExecuteCollect();
96+
var result = this.ExecuteCollect(openMetricsRequested);
9797
if (result)
9898
{
9999
this.previousDataViewGeneratedAtUtc = DateTime.UtcNow;
@@ -168,9 +168,10 @@ private void WaitForReadersToComplete()
168168
}
169169

170170
[MethodImpl(MethodImplOptions.AggressiveInlining)]
171-
private bool ExecuteCollect()
171+
private bool ExecuteCollect(bool openMetricsRequested)
172172
{
173173
this.exporter.OnExport = this.onCollectRef;
174+
this.exporter.OpenMetricsRequested = openMetricsRequested;
174175
var result = this.exporter.Collect(Timeout.Infinite);
175176
this.exporter.OnExport = null;
176177
return result;
@@ -193,7 +194,13 @@ private ExportResult OnCollect(Batch<Metric> metrics)
193194
{
194195
try
195196
{
196-
cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric, this.GetPrometheusMetric(metric));
197+
cursor = PrometheusSerializer.WriteMetric(
198+
this.buffer,
199+
cursor,
200+
metric,
201+
this.GetPrometheusMetric(metric),
202+
this.exporter.OpenMetricsRequested);
203+
197204
break;
198205
}
199206
catch (IndexOutOfRangeException)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ internal Func<Batch<Metric>, ExportResult> OnExport
6363

6464
internal int ScrapeResponseCacheDurationMilliseconds { get; }
6565

66+
internal bool OpenMetricsRequested { get; set; }
67+
6668
/// <inheritdoc/>
6769
public override ExportResult Export(in Batch<Metric> metrics)
6870
{
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// <copyright file="PrometheusHeadersParser.cs" company="OpenTelemetry Authors">
2+
// Copyright The OpenTelemetry Authors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
// </copyright>
16+
17+
namespace OpenTelemetry.Exporter.Prometheus;
18+
19+
internal static class PrometheusHeadersParser
20+
{
21+
private const string OpenMetricsMediaType = "application/openmetrics-text";
22+
23+
internal static bool AcceptsOpenMetrics(string contentType)
24+
{
25+
var value = contentType.AsSpan();
26+
27+
while (value.Length > 0)
28+
{
29+
var headerValue = SplitNext(ref value, ',');
30+
var mediaType = SplitNext(ref headerValue, ';');
31+
32+
if (mediaType.Equals(OpenMetricsMediaType.AsSpan(), StringComparison.Ordinal))
33+
{
34+
return true;
35+
}
36+
}
37+
38+
return false;
39+
}
40+
41+
private static ReadOnlySpan<char> SplitNext(ref ReadOnlySpan<char> span, char character)
42+
{
43+
var index = span.IndexOf(character);
44+
45+
if (index == -1)
46+
{
47+
var part = span;
48+
span = span.Slice(span.Length);
49+
50+
return part;
51+
}
52+
else
53+
{
54+
var part = span.Slice(0, index);
55+
span = span.Slice(index + 1);
56+
57+
return part;
58+
}
59+
}
60+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,32 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric
326326
return cursor;
327327
}
328328

329+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
330+
public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool useOpenMetrics)
331+
{
332+
if (useOpenMetrics)
333+
{
334+
cursor = WriteLong(buffer, cursor, value / 1000);
335+
buffer[cursor++] = unchecked((byte)'.');
336+
337+
long millis = value % 1000;
338+
339+
if (millis < 100)
340+
{
341+
buffer[cursor++] = unchecked((byte)'0');
342+
}
343+
344+
if (millis < 10)
345+
{
346+
buffer[cursor++] = unchecked((byte)'0');
347+
}
348+
349+
return WriteLong(buffer, cursor, millis);
350+
}
351+
352+
return WriteLong(buffer, cursor, value);
353+
}
354+
329355
private static string MapPrometheusType(PrometheusType type)
330356
{
331357
return type switch

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static bool CanWriteMetric(Metric metric)
3535
return true;
3636
}
3737

38-
public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric)
38+
public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false)
3939
{
4040
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric);
4141
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric);
@@ -94,7 +94,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
9494

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

97-
cursor = WriteLong(buffer, cursor, timestamp);
97+
cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested);
9898

9999
buffer[cursor++] = ASCII_LINEFEED;
100100
}
@@ -136,7 +136,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
136136
cursor = WriteLong(buffer, cursor, totalCount);
137137
buffer[cursor++] = unchecked((byte)' ');
138138

139-
cursor = WriteLong(buffer, cursor, timestamp);
139+
cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested);
140140

141141
buffer[cursor++] = ASCII_LINEFEED;
142142
}
@@ -163,7 +163,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
163163
cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum());
164164
buffer[cursor++] = unchecked((byte)' ');
165165

166-
cursor = WriteLong(buffer, cursor, timestamp);
166+
cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested);
167167

168168
buffer[cursor++] = ASCII_LINEFEED;
169169

@@ -189,14 +189,12 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
189189
cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount());
190190
buffer[cursor++] = unchecked((byte)' ');
191191

192-
cursor = WriteLong(buffer, cursor, timestamp);
192+
cursor = WriteTimestamp(buffer, cursor, timestamp, openMetricsRequested);
193193

194194
buffer[cursor++] = ASCII_LINEFEED;
195195
}
196196
}
197197

198-
buffer[cursor++] = ASCII_LINEFEED;
199-
200198
return cursor;
201199
}
202200
}

src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusHttpListener.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ public void Dispose()
110110
}
111111
}
112112

113+
private static bool AcceptsOpenMetrics(HttpListenerRequest request)
114+
{
115+
var acceptHeader = request.Headers["Accept"];
116+
117+
if (string.IsNullOrEmpty(acceptHeader))
118+
{
119+
return false;
120+
}
121+
122+
return PrometheusHeadersParser.AcceptsOpenMetrics(acceptHeader);
123+
}
124+
113125
private void WorkerProc()
114126
{
115127
this.httpListener.Start();
@@ -148,15 +160,19 @@ private async Task ProcessRequestAsync(HttpListenerContext context)
148160
{
149161
try
150162
{
151-
var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false);
163+
var openMetricsRequested = AcceptsOpenMetrics(context.Request);
164+
var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false);
165+
152166
try
153167
{
154168
context.Response.Headers.Add("Server", string.Empty);
155169
if (collectionResponse.View.Count > 0)
156170
{
157171
context.Response.StatusCode = 200;
158172
context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
159-
context.Response.ContentType = "text/plain; charset=utf-8; version=0.0.4";
173+
context.Response.ContentType = openMetricsRequested
174+
? "application/openmetrics-text; version=1.0.0; charset=utf-8"
175+
: "text/plain; charset=utf-8; version=0.0.4";
160176

161177
await context.Response.OutputStream.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false);
162178
}

0 commit comments

Comments
 (0)