Skip to content
Open
Changes from 14 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 @@ -101,6 +101,8 @@ internal abstract class TextFormatSerializer

private string[]? reservedExemplarOutputKeys;

private int serializedTagsBufferHint = 256;

public static OpenMetricsV0Serializer OpenMetricsV0 => field ??= new();

public static OpenMetricsV1Serializer OpenMetricsV1 => field ??= new();
Expand Down Expand Up @@ -229,65 +231,80 @@ public int WriteMetric(
foreach (ref readonly var metricPoint in metric.GetMetricPoints())
{
var tags = metricPoint.Tags;
var serializedTags = this.SerializeTags(metric, tags, options, ReservedHistogramLabelNames);
var hasNegativeBucketBounds = false;
var previousBound = double.NegativeInfinity;

long totalCount = 0;
foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets())
var serializedTagsBuffer = this.SerializeTagsToPooledBuffer(
metric,
tags,
options,
ReservedHistogramLabelNames,
out var serializedTagsLength);

try
{
hasNegativeBucketBounds |= histogramMeasurement.ExplicitBound < 0;
var serializedTags = new ReadOnlySpan<byte>(serializedTagsBuffer, 0, serializedTagsLength);
var hasNegativeBucketBounds = false;
var previousBound = double.NegativeInfinity;

totalCount += histogramMeasurement.BucketCount;
long totalCount = 0;
foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets())
{
hasNegativeBucketBounds |= histogramMeasurement.ExplicitBound < 0;

cursor = this.WriteHistogramBucketName(buffer, cursor, prometheusMetric);
totalCount += histogramMeasurement.BucketCount;

cursor = WriteSerializedTagValues(buffer, cursor, serializedTags, appendTrailingComma: true);
cursor = this.WriteHistogramBucketName(buffer, cursor, prometheusMetric);

cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\"");
cursor = WriteSerializedTagValues(buffer, cursor, serializedTags, appendTrailingComma: true);

if (histogramMeasurement.ExplicitBound != double.PositiveInfinity)
{
cursor = this.WriteExplicitBound(buffer, cursor, histogramMeasurement.ExplicitBound);
}
else
{
cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf");
}
cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\"");

cursor = WriteAsciiStringNoEscape(buffer, cursor, "\"} ");
if (histogramMeasurement.ExplicitBound != double.PositiveInfinity)
{
cursor = this.WriteExplicitBound(buffer, cursor, histogramMeasurement.ExplicitBound);
}
else
{
cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf");
}

cursor = WriteLong(buffer, cursor, totalCount);
cursor = WriteAsciiStringNoEscape(buffer, cursor, "\"} ");

cursor = this.WriteHistogramBucketExemplar(buffer, cursor, in metricPoint, previousBound, histogramMeasurement.ExplicitBound);
cursor = WriteLong(buffer, cursor, totalCount);

buffer[cursor++] = AsciiLineFeed;
previousBound = histogramMeasurement.ExplicitBound;
}
cursor = this.WriteHistogramBucketExemplar(buffer, cursor, in metricPoint, previousBound, histogramMeasurement.ExplicitBound);

if (this.ShouldWriteSumAndCount(hasNegativeBucketBounds))
{
// OpenMetrics histograms with negative bucket thresholds MUST NOT expose
// _sum and therefore MUST NOT expose _count.
// See https://prometheus.io/docs/specs/om/open_metrics_spec/#histogram-1
cursor = this.WriteSeriesNameAndSerializedTags(buffer, cursor, prometheusMetric, "_sum", serializedTags);
buffer[cursor++] = AsciiLineFeed;
previousBound = histogramMeasurement.ExplicitBound;
}

buffer[cursor++] = unchecked((byte)' ');
if (this.ShouldWriteSumAndCount(hasNegativeBucketBounds))
{
// OpenMetrics histograms with negative bucket thresholds MUST NOT expose
// _sum and therefore MUST NOT expose _count.
// See https://prometheus.io/docs/specs/om/open_metrics_spec/#histogram-1
cursor = this.WriteSeriesNameAndSerializedTags(buffer, cursor, prometheusMetric, "_sum", serializedTags);

cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum());
buffer[cursor++] = unchecked((byte)' ');

buffer[cursor++] = AsciiLineFeed;
cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum());

// Histogram count
cursor = this.WriteSeriesNameAndSerializedTags(buffer, cursor, prometheusMetric, "_count", serializedTags);
buffer[cursor++] = AsciiLineFeed;

buffer[cursor++] = unchecked((byte)' ');
// Histogram count
cursor = this.WriteSeriesNameAndSerializedTags(buffer, cursor, prometheusMetric, "_count", serializedTags);

cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount());
buffer[cursor++] = AsciiLineFeed;
}
buffer[cursor++] = unchecked((byte)' ');

cursor = this.WriteHistogramCreated(buffer, cursor, metric, prometheusMetric, in metricPoint, in options);
cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount());
buffer[cursor++] = AsciiLineFeed;
}

cursor = this.WriteHistogramCreated(buffer, cursor, metric, prometheusMetric, in metricPoint, in options);
}
finally
{
ArrayPool<byte>.Shared.Return(serializedTagsBuffer);
}
}
}

Expand Down Expand Up @@ -788,77 +805,88 @@ internal int WriteTags(
// The fast path writes each output label name directly into the buffer and detects reserved
// names and collisions by comparing the written bytes, so no per-label key string is
// allocated. Each entry is the (start, length) of an already-written key within the buffer.
List<(int Start, int Length)>? writtenKeyRanges = null;
(int Start, int Length)[]? writtenKeyRanges = null;
var writtenKeyRangeCount = 0;
var wroteLabel = false;

var resourceConstantLabels = options.ResourceConstantLabels;
var hasResourceConstantLabels = resourceConstantLabels is { Count: > 0 };

if (writeEnclosingBraces)
{
buffer[cursor++] = unchecked((byte)'{');
}

// The fast path writes scope labels and point tags directly to the buffer. It cannot
// account for resource constant labels (which may collide with, and therefore need to be
// merged with, point tags), so it is skipped whenever any are present.
if (!hasResourceConstantLabels)
try
{
if (!quotedNameBytes.IsEmpty)
if (writeEnclosingBraces)
{
cursor = WriteQuotedName(buffer, cursor, quotedNameBytes, quotedNameSuffix);
wroteLabel = true;
buffer[cursor++] = unchecked((byte)'{');
}

if (!options.SuppressScopeInfo)
// The fast path writes scope labels and point tags directly to the buffer. It cannot
// account for resource constant labels (which may collide with, and therefore need to be
// merged with, point tags), so it is skipped whenever any are present.
if (!hasResourceConstantLabels)
{
WriteScopeLabels();
}
if (!quotedNameBytes.IsEmpty)
{
cursor = WriteQuotedName(buffer, cursor, quotedNameBytes, quotedNameSuffix);
wroteLabel = true;
}

if (TryWritePointTags())
{
if (writeEnclosingBraces)
if (!options.SuppressScopeInfo)
{
buffer[cursor++] = unchecked((byte)'}');
WriteScopeLabels();
}
else if (wroteLabel)

if (TryWritePointTags())
{
buffer[cursor++] = unchecked((byte)',');
if (writeEnclosingBraces)
{
buffer[cursor++] = unchecked((byte)'}');
}
else if (wroteLabel)
{
buffer[cursor++] = unchecked((byte)',');
}

return cursor;
}
}

return cursor;
cursor = startCursor;
List<LabelData>? labels = null;

if (!options.SuppressScopeInfo)
{
foreach (var scopeLabel in GetScopeLabelData(metric))
{
// Scope labels (otel_scope_*) are already in their target Prometheus form, so they
// are written verbatim and not re-escaped by the negotiated scheme, exactly as the
// fast path does.
AddLabel(scopeLabel.OriginalKey, scopeLabel.OutputKey, scopeLabel.Value, ref labels, reservedOutputKeys);
}
}
}

cursor = startCursor;
List<LabelData>? labels = null;
foreach (var tag in tags)
{
this.AddLabel(tag.Key, tag.Value, ref labels, reservedOutputKeys);
}

if (!options.SuppressScopeInfo)
{
foreach (var scopeLabel in GetScopeLabelData(metric))
if (hasResourceConstantLabels)
{
// Scope labels (otel_scope_*) are already in their target Prometheus form, so they
// are written verbatim and not re-escaped by the negotiated scheme, exactly as the
// fast path does.
AddLabel(scopeLabel.OriginalKey, scopeLabel.OutputKey, scopeLabel.Value, ref labels, reservedOutputKeys);
foreach (var resourceLabel in resourceConstantLabels!)
{
this.AddLabel(resourceLabel.Key, resourceLabel.Value, ref labels, reservedOutputKeys);
}
}
}

foreach (var tag in tags)
{
this.AddLabel(tag.Key, tag.Value, ref labels, reservedOutputKeys);
return WriteLabels(buffer, cursor, labels, writeEnclosingBraces, quotedNameBytes, quotedNameSuffix);
}

if (hasResourceConstantLabels)
finally
{
foreach (var resourceLabel in resourceConstantLabels!)
if (writtenKeyRanges != null)
{
this.AddLabel(resourceLabel.Key, resourceLabel.Value, ref labels, reservedOutputKeys);
ArrayPool<(int, int)>.Shared.Return(writtenKeyRanges);
}
}

return WriteLabels(buffer, cursor, labels, writeEnclosingBraces, quotedNameBytes, quotedNameSuffix);

void WriteScopeLabels()
{
// Scope labels (otel_scope_*) are OpenTelemetry naming conventions that are already
Expand Down Expand Up @@ -915,20 +943,33 @@ bool TryWriteLabel(string key, object? value, bool isScopeLabel = false)
return true;
}

if (writtenKeyRanges != null)
for (var i = 0; i < writtenKeyRangeCount; i++)
{
foreach (var (start, length) in writtenKeyRanges)
var (start, length) = writtenKeyRanges![i];
if (new ReadOnlySpan<byte>(buffer, start, length).SequenceEqual(writtenKey))
{
if (new ReadOnlySpan<byte>(buffer, start, length).SequenceEqual(writtenKey))
{
cursor = rewindCursor;
return false;
}
cursor = rewindCursor;
return false;
}
}

writtenKeyRanges ??= [];
writtenKeyRanges.Add((keyStart, writtenKey.Length));
var pool = ArrayPool<(int, int)>.Shared;

if (writtenKeyRanges == null)
{
writtenKeyRanges = pool.Rent(8);
}
else if (writtenKeyRangeCount == writtenKeyRanges.Length)
{
var grown = pool.Rent(writtenKeyRanges.Length * 2);

Array.Copy(writtenKeyRanges, grown, writtenKeyRangeCount);
pool.Return(writtenKeyRanges);

writtenKeyRanges = grown;
}

writtenKeyRanges[writtenKeyRangeCount++] = (keyStart, writtenKey.Length);

cursor = WriteSanitizedLabel(buffer, cursor, value);
wroteLabel = true;
Expand All @@ -937,13 +978,15 @@ bool TryWriteLabel(string key, object? value, bool isScopeLabel = false)
}
}

internal byte[] SerializeTags(
internal byte[] SerializeTagsToPooledBuffer(
Metric metric,
ReadOnlyTagCollection tags,
in TextFormatSerializerOptions options,
IReadOnlyCollection<string>? reservedOutputKeys = null)
IReadOnlyCollection<string>? reservedOutputKeys,
out int length)
{
var buffer = new byte[128];
var pool = ArrayPool<byte>.Shared;
var buffer = pool.Rent(Volatile.Read(ref this.serializedTagsBufferHint));

Comment thread
martincostello marked this conversation as resolved.
while (true)
{
Expand All @@ -963,11 +1006,21 @@ internal byte[] SerializeTags(
cursor--;
}

return buffer.AsSpan(0, cursor).ToArray();
length = cursor;

if (buffer.Length > Volatile.Read(ref this.serializedTagsBufferHint))
{
Volatile.Write(ref this.serializedTagsBufferHint, buffer.Length);
}
Comment thread
martincostello marked this conversation as resolved.
Outdated

return buffer;
}
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
{
buffer = new byte[GetNextSerializedTagsBufferSize(buffer.Length)];
var nextBufferSize = GetNextSerializedTagsBufferSize(buffer.Length);

pool.Return(buffer);
buffer = pool.Rent(nextBufferSize);
}
}
}
Expand Down