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

Export OpenMetrics format for prometheus exporters #5107

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1c02405
Export openmetrics format for prometheus exporters
robertcoltheart Dec 1, 2023
5a98791
Update changelog
robertcoltheart Dec 1, 2023
36eb712
Add serializer test
robertcoltheart Dec 1, 2023
3c39d4c
Merge branch 'main' into feature/export-prometheus-openmetrics
robertcoltheart Dec 1, 2023
438ce9c
Merge branch 'main' into feature/export-prometheus-openmetrics
robertcoltheart Dec 1, 2023
945834a
Decide content type based on accept header
robertcoltheart Dec 2, 2023
97a1511
Merge branch 'feature/export-prometheus-openmetrics' of https://githu…
robertcoltheart Dec 2, 2023
a8374af
Tidy test variable naming
robertcoltheart Dec 2, 2023
ee44164
Merge branch 'main' into feature/export-prometheus-openmetrics
robertcoltheart Dec 2, 2023
8b9e72e
Use fewer allocations for media type parsing
robertcoltheart Dec 5, 2023
85ec4a6
Merge branch 'main' into feature/export-prometheus-openmetrics
robertcoltheart Dec 5, 2023
6b4b86f
Update src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExp…
robertcoltheart Dec 6, 2023
82d1921
Fix build
robertcoltheart Dec 6, 2023
547782f
Update src/OpenTelemetry.Exporter.Prometheus.AspNetCore/PrometheusExp…
robertcoltheart Dec 6, 2023
3130372
Fix build
robertcoltheart Dec 6, 2023
36df8f8
Update src/OpenTelemetry.Exporter.Prometheus.HttpListener/PrometheusH…
robertcoltheart Dec 6, 2023
6558ca0
Refactor and share more code, fix flaky tests
robertcoltheart Dec 6, 2023
5a939cd
Merge branch 'main' into feature/export-prometheus-openmetrics
robertcoltheart Dec 6, 2023
75e0bbb
Tidy usings
robertcoltheart Dec 6, 2023
75df04a
Refactor tests
robertcoltheart Dec 6, 2023
bff3601
Fix build
robertcoltheart Dec 6, 2023
5ec4852
Merge branch 'main' into feature/export-prometheus-openmetrics
robertcoltheart Dec 6, 2023
8dd0cbf
Make static and remove redundant set
robertcoltheart Dec 6, 2023
188d59d
Oops fix build
robertcoltheart Dec 6, 2023
015ad3b
Use foreach instead of linq
robertcoltheart Dec 6, 2023
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 @@ -2,6 +2,8 @@

## Unreleased

* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107))

## 1.7.0-rc.1

Released 2023-Nov-29
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using OpenTelemetry.Exporter.Prometheus;
using OpenTelemetry.Exporter.Prometheus.AspNetCore;
using OpenTelemetry.Internal;
using OpenTelemetry.Metrics;

Expand All @@ -27,6 +30,8 @@ namespace OpenTelemetry.Exporter;
/// </summary>
internal sealed class PrometheusExporterMiddleware
{
private const string OpenMetricsMediaType = "application/openmetrics-text";

private readonly PrometheusExporter exporter;

/// <summary>
Expand Down Expand Up @@ -64,7 +69,9 @@ public async Task InvokeAsync(HttpContext httpContext)

try
{
var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false);
var openMetricsRequested = this.AcceptsOpenMetrics(httpContext.Request);
var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false);

try
{
if (collectionResponse.View.Count > 0)
Expand All @@ -75,7 +82,9 @@ public async Task InvokeAsync(HttpContext httpContext)
#else
response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
#endif
response.ContentType = "text/plain; charset=utf-8; version=0.0.4";
response.ContentType = openMetricsRequested
? "application/openmetrics-text; version=1.0.0; charset=utf-8"
: "text/plain; charset=utf-8; version=0.0.4";

await response.Body.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false);
}
Expand All @@ -102,4 +111,32 @@ public async Task InvokeAsync(HttpContext httpContext)

this.exporter.OnExport = null;
}

private bool AcceptsOpenMetrics(HttpRequest request)
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved
{
var acceptHeader = request.Headers.Accept;

if (StringValues.IsNullOrEmpty(acceptHeader))
{
return false;
}

foreach (var accept in acceptHeader)
{
var value = accept.AsSpan();

while (value.Length > 0)
{
var headerValue = value.SplitNext(',');
var mediaType = headerValue.SplitNext(';');

if (mediaType.Equals(OpenMetricsMediaType, StringComparison.Ordinal))
{
return true;
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// <copyright file="ReadOnlySpanExtensions.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

namespace OpenTelemetry.Exporter.Prometheus.AspNetCore;

internal static class ReadOnlySpanExtensions
{
internal static ReadOnlySpan<char> SplitNext(this ref ReadOnlySpan<char> span, char character)
{
var index = span.IndexOf(character);

if (index == -1)
{
var part = span;
span = span.Slice(span.Length);

return part;
}
else
{
var part = span.Slice(0, index);
span = span.Slice(index + 1);

return part;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Export OpenMetrics format from Prometheus exporters ([#5107](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5107))

## 1.7.0-rc.1

Released 2023-Nov-29
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ public PrometheusCollectionManager(PrometheusExporter exporter)
}

#if NET6_0_OR_GREATER
public ValueTask<CollectionResponse> EnterCollect()
public ValueTask<CollectionResponse> EnterCollect(bool openMetricsRequested)
#else
public Task<CollectionResponse> EnterCollect()
public Task<CollectionResponse> EnterCollect(bool openMetricsRequested)
#endif
{
this.EnterGlobalLock();
Expand Down Expand Up @@ -93,7 +93,7 @@ public Task<CollectionResponse> EnterCollect()
this.ExitGlobalLock();

CollectionResponse response;
var result = this.ExecuteCollect();
var result = this.ExecuteCollect(openMetricsRequested);
if (result)
{
this.previousDataViewGeneratedAtUtc = DateTime.UtcNow;
Expand Down Expand Up @@ -168,11 +168,13 @@ private void WaitForReadersToComplete()
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ExecuteCollect()
private bool ExecuteCollect(bool openMetricsRequested)
{
this.exporter.OnExport = this.onCollectRef;
this.exporter.OpenMetricsRequested = openMetricsRequested;
var result = this.exporter.Collect(Timeout.Infinite);
this.exporter.OnExport = null;
this.exporter.OpenMetricsRequested = null;
return result;
}

Expand All @@ -193,7 +195,13 @@ private ExportResult OnCollect(Batch<Metric> metrics)
{
try
{
cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric, this.GetPrometheusMetric(metric));
cursor = PrometheusSerializer.WriteMetric(
this.buffer,
cursor,
metric,
this.GetPrometheusMetric(metric),
this.exporter.OpenMetricsRequested ?? false);

break;
}
catch (IndexOutOfRangeException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ internal Func<Batch<Metric>, ExportResult> OnExport

internal int ScrapeResponseCacheDurationMilliseconds { get; }

internal bool? OpenMetricsRequested { get; set; }
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved

/// <inheritdoc/>
public override ExportResult Export(in Batch<Metric> metrics)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,32 @@ public static int WriteUnitMetadata(byte[] buffer, int cursor, PrometheusMetric
return cursor;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteTimestamp(byte[] buffer, int cursor, long value, bool useOpenMetrics)
{
if (useOpenMetrics)
{
cursor = WriteLong(buffer, cursor, value / 1000);
buffer[cursor++] = unchecked((byte)'.');

long millis = value % 1000;

if (millis < 100)
{
buffer[cursor++] = unchecked((byte)'0');
}

if (millis < 10)
{
buffer[cursor++] = unchecked((byte)'0');
}

return WriteLong(buffer, cursor, millis);
}

return WriteLong(buffer, cursor, value);
}

private static string MapPrometheusType(PrometheusType type)
{
return type switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static bool CanWriteMetric(Metric metric)
return true;
}

public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric)
public static int WriteMetric(byte[] buffer, int cursor, Metric metric, PrometheusMetric prometheusMetric, bool openMetricsRequested = false)
{
cursor = WriteTypeMetadata(buffer, cursor, prometheusMetric);
cursor = WriteUnitMetadata(buffer, cursor, prometheusMetric);
Expand Down Expand Up @@ -94,7 +94,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe

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

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

buffer[cursor++] = ASCII_LINEFEED;
}
Expand Down Expand Up @@ -136,7 +136,7 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric, Promethe
cursor = WriteLong(buffer, cursor, totalCount);
buffer[cursor++] = unchecked((byte)' ');

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

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

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

buffer[cursor++] = ASCII_LINEFEED;

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

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

buffer[cursor++] = ASCII_LINEFEED;
}
}

buffer[cursor++] = ASCII_LINEFEED;
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved

return cursor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace OpenTelemetry.Exporter;

internal sealed class PrometheusHttpListener : IDisposable
{
private const string OpenMetricsMediaType = "application/openmetrics-text";
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved

private readonly PrometheusExporter exporter;
private readonly HttpListener httpListener = new();
private readonly object syncObject = new();
Expand Down Expand Up @@ -148,15 +150,19 @@ private async Task ProcessRequestAsync(HttpListenerContext context)
{
try
{
var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false);
var openMetricsRequested = this.AcceptsOpenMetrics(context.Request);
var collectionResponse = await this.exporter.CollectionManager.EnterCollect(openMetricsRequested).ConfigureAwait(false);

try
{
context.Response.Headers.Add("Server", string.Empty);
if (collectionResponse.View.Count > 0)
{
context.Response.StatusCode = 200;
context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R"));
context.Response.ContentType = "text/plain; charset=utf-8; version=0.0.4";
context.Response.ContentType = openMetricsRequested
? "application/openmetrics-text; version=1.0.0; charset=utf-8"
: "text/plain; charset=utf-8; version=0.0.4";

await context.Response.OutputStream.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false);
}
Expand Down Expand Up @@ -187,4 +193,36 @@ private async Task ProcessRequestAsync(HttpListenerContext context)
{
}
}

private bool AcceptsOpenMetrics(HttpListenerRequest request)
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved
utpilla marked this conversation as resolved.
Show resolved Hide resolved
{
if (request.AcceptTypes == null)
{
return false;
}

foreach (var acceptType in request.AcceptTypes)
{
if (this.GetMediaType(acceptType) == OpenMetricsMediaType)
{
return true;
}
}

return false;
}

private string GetMediaType(string acceptHeader)
{
if (string.IsNullOrEmpty(acceptHeader))
{
return string.Empty;
}

var separatorIndex = acceptHeader.IndexOf(';');

return separatorIndex == -1
? acceptHeader
: acceptHeader.Substring(0, separatorIndex);
robertcoltheart marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading