prometheus-net

Форк
0
/
TextSerializer.Net.cs 
619 строк · 25.7 Кб
1
#if NET
2
using System;
3
using System.Buffers;
4
using System.Globalization;
5
using System.Runtime.CompilerServices;
6

7
namespace Prometheus;
8

9
/// <remarks>
10
/// Does NOT take ownership of the stream - caller remains the boss.
11
/// </remarks>
12
internal sealed class TextSerializer : IMetricsSerializer
13
{
14
    internal static ReadOnlySpan<byte> NewLine => [(byte)'\n'];
15
    internal static ReadOnlySpan<byte> Quote => [(byte)'"'];
16
    internal static ReadOnlySpan<byte> Equal => [(byte)'='];
17
    internal static ReadOnlySpan<byte> Comma => [(byte)','];
18
    internal static ReadOnlySpan<byte> Underscore => [(byte)'_'];
19
    internal static ReadOnlySpan<byte> LeftBrace => [(byte)'{'];
20
    internal static ReadOnlySpan<byte> RightBraceSpace => [(byte)'}', (byte)' '];
21
    internal static ReadOnlySpan<byte> Space => [(byte)' '];
22
    internal static ReadOnlySpan<byte> SpaceHashSpaceLeftBrace => [(byte)' ', (byte)'#', (byte)' ', (byte)'{'];
23
    internal static ReadOnlySpan<byte> PositiveInfinity => [(byte)'+', (byte)'I', (byte)'n', (byte)'f'];
24
    internal static ReadOnlySpan<byte> NegativeInfinity => [(byte)'-', (byte)'I', (byte)'n', (byte)'f'];
25
    internal static ReadOnlySpan<byte> NotANumber => [(byte)'N', (byte)'a', (byte)'N'];
26
    internal static ReadOnlySpan<byte> DotZero => [(byte)'.', (byte)'0'];
27
    internal static ReadOnlySpan<byte> FloatPositiveOne => [(byte)'1', (byte)'.', (byte)'0'];
28
    internal static ReadOnlySpan<byte> FloatZero => [(byte)'0', (byte)'.', (byte)'0'];
29
    internal static ReadOnlySpan<byte> FloatNegativeOne => [(byte)'-', (byte)'1', (byte)'.', (byte)'0'];
30
    internal static ReadOnlySpan<byte> IntPositiveOne => [(byte)'1'];
31
    internal static ReadOnlySpan<byte> IntZero => [(byte)'0'];
32
    internal static ReadOnlySpan<byte> IntNegativeOne => [(byte)'-', (byte)'1'];
33
    internal static ReadOnlySpan<byte> HashHelpSpace => [(byte)'#', (byte)' ', (byte)'H', (byte)'E', (byte)'L', (byte)'P', (byte)' '];
34
    internal static ReadOnlySpan<byte> NewlineHashTypeSpace => [(byte)'\n', (byte)'#', (byte)' ', (byte)'T', (byte)'Y', (byte)'P', (byte)'E', (byte)' '];
35

36
    internal static readonly byte[] UnknownBytes = "unknown"u8.ToArray();
37
    internal static readonly byte[] EofNewLineBytes = [(byte)'#', (byte)' ', (byte)'E', (byte)'O', (byte)'F', (byte)'\n'];
38
    internal static readonly byte[] PositiveInfinityBytes = [(byte)'+', (byte)'I', (byte)'n', (byte)'f'];
39

40
    internal static readonly Dictionary<MetricType, byte[]> MetricTypeToBytes = new()
41
    {
42
        { MetricType.Gauge, "gauge"u8.ToArray() },
43
        { MetricType.Counter, "counter"u8.ToArray() },
44
        { MetricType.Histogram, "histogram"u8.ToArray() },
45
        { MetricType.Summary, "summary"u8.ToArray() },
46
    };
47

48
    private static readonly char[] DotEChar = ['.', 'e'];
49

50
    public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText)
51
    {
52
        _expositionFormat = fmt;
53
        _stream = new Lazy<Stream>(() => AddStreamBuffering(stream));
54
    }
55

56
    // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior.
57
    public TextSerializer(Func<Stream> streamFactory,
58
        ExpositionFormat fmt = ExpositionFormat.PrometheusText)
59
    {
60
        _expositionFormat = fmt;
61
        _stream = new Lazy<Stream>(() => AddStreamBuffering(streamFactory()));
62
    }
63

64
    /// <summary>
65
    /// Ensures that writes to the stream are buffered, meaning we do not emit individual "write 1 byte" calls to the stream.
66
    /// This has been rumored by some users to be relevant in their scenarios (though never with solid evidence or repro steps).
67
    /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing
68
    /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform.
69
    /// </summary>
70
    private static Stream AddStreamBuffering(Stream inner)
71
    {
72
        return new BufferedStream(inner, bufferSize: 16 * 1024);
73
    }
74

75
    public async Task FlushAsync(CancellationToken cancel)
76
    {
77
        // If we never opened the stream, we don't touch it on flush.
78
        if (!_stream.IsValueCreated)
79
            return;
80

81
        await _stream.Value.FlushAsync(cancel);
82
    }
83

84
    private readonly Lazy<Stream> _stream;
85

86
    [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
87
    public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type,
88
        byte[] typeBytes, CancellationToken cancel)
89
    {
90
        var bufferLength = MeasureFamilyDeclarationLength(name, nameBytes, helpBytes, type, typeBytes);
91
        var buffer = ArrayPool<byte>.Shared.Rent(bufferLength);
92

93
        try
94
        {
95
            var nameLen = nameBytes.Length;
96
            if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter)
97
            {
98
                if (name.EndsWith("_total"))
99
                {
100
                    nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix.
101
                }
102
                else
103
                {
104
                    typeBytes = UnknownBytes; // if the total prefix is missing the _total prefix it is out of spec
105
                }
106
            }
107

108
            var position = 0;
109
            AppendToBufferAndIncrementPosition(HashHelpSpace, buffer, ref position);
110
            AppendToBufferAndIncrementPosition(nameBytes.AsSpan(0, nameLen), buffer, ref position);
111
            // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text.
112
            AppendToBufferAndIncrementPosition(Space, buffer, ref position);
113
            if (helpBytes.Length > 0)
114
            {
115
                AppendToBufferAndIncrementPosition(helpBytes, buffer, ref position);
116
            }
117
            AppendToBufferAndIncrementPosition(NewlineHashTypeSpace, buffer, ref position);
118
            AppendToBufferAndIncrementPosition(nameBytes.AsSpan(0, nameLen), buffer, ref position);
119
            AppendToBufferAndIncrementPosition(Space, buffer, ref position);
120
            AppendToBufferAndIncrementPosition(typeBytes, buffer, ref position);
121
            AppendToBufferAndIncrementPosition(NewLine, buffer, ref position);
122

123
            await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel);
124
        }
125
        finally
126
        {
127
            ArrayPool<byte>.Shared.Return(buffer);
128
        }
129
    }
130

131
    public int MeasureFamilyDeclarationLength(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes)
132
    {
133
        // We mirror the logic in the Write() call but just measure how many bytes of buffer we need.
134
        var length = 0;
135

136
        var nameLen = nameBytes.Length;
137

138
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter)
139
        {
140
            if (name.EndsWith("_total"))
141
            {
142
                nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix.
143
            }
144
            else
145
            {
146
                typeBytes = UnknownBytes; // if the total prefix is missing the _total prefix it is out of spec
147
            }
148
        }
149

150
        length += HashHelpSpace.Length;
151
        length += nameLen;
152
        // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text.
153
        length += Space.Length;
154
        length += helpBytes.Length;
155
        length += NewlineHashTypeSpace.Length;
156
        length += nameLen;
157
        length += Space.Length;
158
        length += typeBytes.Length;
159
        length += NewLine.Length;
160

161
        return length;
162
    }
163

164
    public async ValueTask WriteEnd(CancellationToken cancel)
165
    {
166
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
167
            await _stream.Value.WriteAsync(EofNewLineBytes, cancel);
168
    }
169

170
    [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
171
    public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel,
172
        double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel)
173
    {
174
        // This is a max length because we do not know ahead of time how many bytes the actual value will consume.
175
        var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length;
176

177
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid)
178
            bufferMaxLength += MeasureExemplarMaxLength(exemplar);
179

180
        var buffer = ArrayPool<byte>.Shared.Rent(bufferMaxLength);
181

182
        try
183
        {
184
            var position = WriteIdentifierPart(buffer, name, flattenedLabels, canonicalLabel, suffix);
185

186
            position += WriteValue(buffer.AsSpan(position..), value);
187

188
            if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid)
189
            {
190
                position += WriteExemplar(buffer.AsSpan(position..), exemplar);
191
            }
192

193
            AppendToBufferAndIncrementPosition(NewLine, buffer, ref position);
194

195
            ValidateBufferMaxLengthAndPosition(bufferMaxLength, position);
196

197
            await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel);
198
        }
199
        finally
200
        {
201
            ArrayPool<byte>.Shared.Return(buffer);
202
        }
203
    }
204

205
    [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
206
    public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel,
207
        long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel)
208
    {
209
        // This is a max length because we do not know ahead of time how many bytes the actual value will consume.
210
        var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length;
211

212
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid)
213
            bufferMaxLength += MeasureExemplarMaxLength(exemplar);
214

215
        var buffer = ArrayPool<byte>.Shared.Rent(bufferMaxLength);
216

217
        try
218
        {
219
            var position = WriteIdentifierPart(buffer, name, flattenedLabels, canonicalLabel, suffix);
220

221
            position += WriteValue(buffer.AsSpan(position..), value);
222

223
            if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid)
224
            {
225
                position += WriteExemplar(buffer.AsSpan(position..), exemplar);
226
            }
227

228
            AppendToBufferAndIncrementPosition(NewLine, buffer, ref position);
229

230
            ValidateBufferMaxLengthAndPosition(bufferMaxLength, position);
231

232
            await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel);
233
        }
234
        finally
235
        {
236
            ArrayPool<byte>.Shared.Return(buffer);
237
        }
238
    }
239

240
    private int WriteExemplar(Span<byte> buffer, ObservedExemplar exemplar)
241
    {
242
        var position = 0;
243

244
        AppendToBufferAndIncrementPosition(SpaceHashSpaceLeftBrace, buffer, ref position);
245

246
        for (var i = 0; i < exemplar.Labels!.Length; i++)
247
        {
248
            if (i > 0)
249
                AppendToBufferAndIncrementPosition(Comma, buffer, ref position);
250

251
            ref var labelPair = ref exemplar.Labels[i];
252
            position += WriteExemplarLabel(buffer[position..], labelPair.KeyBytes, labelPair.Value);
253
        }
254

255
        AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position);
256
        position += WriteValue(buffer[position..], exemplar.Value);
257
        AppendToBufferAndIncrementPosition(Space, buffer, ref position);
258
        position += WriteValue(buffer[position..], exemplar.Timestamp);
259

260
        return position;
261
    }
262

263
    private int MeasureExemplarMaxLength(ObservedExemplar exemplar)
264
    {
265
        // We mirror the logic in the Write() call but just measure how many bytes of buffer we need.
266
        var length = 0;
267

268
        length += SpaceHashSpaceLeftBrace.Length;
269

270
        for (var i = 0; i < exemplar.Labels!.Length; i++)
271
        {
272
            if (i > 0)
273
                length += Comma.Length;
274

275
            ref var labelPair = ref exemplar.Labels[i];
276
            length += MeasureExemplarLabelMaxLength(labelPair.KeyBytes, labelPair.Value);
277
        }
278

279
        length += RightBraceSpace.Length;
280
        length += MeasureValueMaxLength(exemplar.Value);
281
        length += Space.Length;
282
        length += MeasureValueMaxLength(exemplar.Timestamp);
283

284
        return length;
285
    }
286

287
    private static int WriteExemplarLabel(Span<byte> buffer, byte[] label, string value)
288
    {
289
        var position = 0;
290

291
        AppendToBufferAndIncrementPosition(label, buffer, ref position);
292
        AppendToBufferAndIncrementPosition(Equal, buffer, ref position);
293
        AppendToBufferAndIncrementPosition(Quote, buffer, ref position);
294
        position += PrometheusConstants.ExemplarEncoding.GetBytes(value.AsSpan(), buffer[position..]);
295
        AppendToBufferAndIncrementPosition(Quote, buffer, ref position);
296

297
        return position;
298
    }
299

300
    private static int MeasureExemplarLabelMaxLength(byte[] label, string value)
301
    {
302
        // We mirror the logic in the Write() call but just measure how many bytes of buffer we need.
303
        var length = 0;
304

305
        length += label.Length;
306
        length += Equal.Length;
307
        length += Quote.Length;
308
        length += PrometheusConstants.ExemplarEncoding.GetMaxByteCount(value.Length);
309
        length += Quote.Length;
310

311
        return length;
312
    }
313

314
    private int WriteValue(Span<byte> buffer, double value)
315
    {
316
        var position = 0;
317

318
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
319
        {
320
            switch (value)
321
            {
322
                case 0:
323
                    AppendToBufferAndIncrementPosition(FloatZero, buffer, ref position);
324
                    return position;
325
                case 1:
326
                    AppendToBufferAndIncrementPosition(FloatPositiveOne, buffer, ref position);
327
                    return position;
328
                case -1:
329
                    AppendToBufferAndIncrementPosition(FloatNegativeOne, buffer, ref position);
330
                    return position;
331
                case double.PositiveInfinity:
332
                    AppendToBufferAndIncrementPosition(PositiveInfinity, buffer, ref position);
333
                    return position;
334
                case double.NegativeInfinity:
335
                    AppendToBufferAndIncrementPosition(NegativeInfinity, buffer, ref position);
336
                    return position;
337
                case double.NaN:
338
                    AppendToBufferAndIncrementPosition(NotANumber, buffer, ref position);
339
                    return position;
340
            }
341
        }
342

343
        // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd
344
        if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "g", CultureInfo.InvariantCulture))
345
            throw new Exception("Failed to encode floating point value as string.");
346

347
        var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0);
348
        AppendToBufferAndIncrementPosition(_stringBytesBuffer.AsSpan(0, encodedBytes), buffer, ref position);
349

350
        // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics.
351
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresOpenMetricsDotZero(_stringCharsBuffer, charsWritten))
352
            AppendToBufferAndIncrementPosition(DotZero, buffer, ref position);
353

354
        return position;
355
    }
356

357
    static bool RequiresOpenMetricsDotZero(char[] buffer, int length)
358
    {
359
        return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e, so needs a .0 to turn it into a floating-point value */
360
    }
361

362
    private int MeasureValueMaxLength(double value)
363
    {
364
        // We mirror the logic in the Write() call but just measure how many bytes of buffer we need.
365
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
366
        {
367
            switch (value)
368
            {
369
                case 0:
370
                    return FloatZero.Length;
371
                case 1:
372
                    return FloatPositiveOne.Length;
373
                case -1:
374
                    return FloatNegativeOne.Length;
375
                case double.PositiveInfinity:
376
                    return PositiveInfinity.Length;
377
                case double.NegativeInfinity:
378
                    return NegativeInfinity.Length;
379
                case double.NaN:
380
                    return NotANumber.Length;
381
            }
382
        }
383

384
        // We do not want to spend time formatting the value just to measure the length and throw away the result.
385
        // Therefore we just consider the max length and return it. The max length is just the length of the value-encoding buffer.
386
        return _stringBytesBuffer.Length;
387
    }
388

389
    private int WriteValue(Span<byte> buffer, long value)
390
    {
391
        var position = 0;
392

393
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
394
        {
395
            switch (value)
396
            {
397
                case 0:
398
                    AppendToBufferAndIncrementPosition(IntZero, buffer, ref position);
399
                    return position;
400
                case 1:
401
                    AppendToBufferAndIncrementPosition(IntPositiveOne, buffer, ref position);
402
                    return position;
403
                case -1:
404
                    AppendToBufferAndIncrementPosition(IntNegativeOne, buffer, ref position);
405
                    return position;
406
            }
407
        }
408

409
        if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "D", CultureInfo.InvariantCulture))
410
            throw new Exception("Failed to encode integer value as string.");
411

412
        var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0);
413
        AppendToBufferAndIncrementPosition(_stringBytesBuffer.AsSpan(0, encodedBytes), buffer, ref position);
414

415
        return position;
416
    }
417

418
    private int MeasureValueMaxLength(long value)
419
    {
420
        // We mirror the logic in the Write() call but just measure how many bytes of buffer we need.
421
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
422
        {
423
            switch (value)
424
            {
425
                case 0:
426
                    return IntZero.Length;
427
                case 1:
428
                    return IntPositiveOne.Length;
429
                case -1:
430
                    return IntNegativeOne.Length;
431
            }
432
        }
433

434
        // We do not want to spend time formatting the value just to measure the length and throw away the result.
435
        // Therefore we just consider the max length and return it. The max length is just the length of the value-encoding buffer.
436
        return _stringBytesBuffer.Length;
437
    }
438

439
    // Reuse a buffer to do the serialization and UTF-8 encoding.
440
    // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd
441
    private readonly char[] _stringCharsBuffer = new char[32];
442
    private readonly byte[] _stringBytesBuffer = new byte[32];
443

444
    private readonly ExpositionFormat _expositionFormat;
445

446
    private static void AppendToBufferAndIncrementPosition(ReadOnlySpan<byte> from, Span<byte> to, ref int position)
447
    {
448
        from.CopyTo(to[position..]);
449
        position += from.Length;
450
    }
451
    
452
    private static void ValidateBufferLengthAndPosition(int bufferLength, int position)
453
    {
454
        if (position != bufferLength)
455
            throw new Exception("Internal error: counting the same bytes twice got us a different value.");
456
    }
457

458
    private static void ValidateBufferMaxLengthAndPosition(int bufferMaxLength, int position)
459
    {
460
        if (position > bufferMaxLength)
461
            throw new Exception("Internal error: counting the same bytes twice got us a different value.");
462
    }
463

464
    /// <summary>
465
    /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end.
466
    /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"}
467
    /// Note: Terminates with a SPACE
468
    /// </summary>
469
    private int WriteIdentifierPart(Span<byte> buffer, byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, byte[]? suffix = null)
470
    {
471
        var position = 0;
472

473
        AppendToBufferAndIncrementPosition(name, buffer, ref position);
474

475
        if (suffix != null && suffix.Length > 0)
476
        {
477
            AppendToBufferAndIncrementPosition(Underscore, buffer, ref position);
478
            AppendToBufferAndIncrementPosition(suffix, buffer, ref position);
479
        }
480

481
        if (flattenedLabels.Length > 0 || extraLabel.IsNotEmpty)
482
        {
483
            AppendToBufferAndIncrementPosition(LeftBrace, buffer, ref position);
484
            if (flattenedLabels.Length > 0)
485
            {
486
                AppendToBufferAndIncrementPosition(flattenedLabels, buffer, ref position);
487
            }
488

489
            // Extra labels go to the end (i.e. they are deepest to inherit from).
490
            if (extraLabel.IsNotEmpty)
491
            {
492
                if (flattenedLabels.Length > 0)
493
                {
494
                    AppendToBufferAndIncrementPosition(Comma, buffer, ref position);
495
                }
496

497
                AppendToBufferAndIncrementPosition(extraLabel.Name, buffer, ref position);
498
                AppendToBufferAndIncrementPosition(Equal, buffer, ref position);
499
                AppendToBufferAndIncrementPosition(Quote, buffer, ref position);
500

501
                if (_expositionFormat == ExpositionFormat.OpenMetricsText)
502
                    AppendToBufferAndIncrementPosition(extraLabel.OpenMetrics, buffer, ref position);
503
                else
504
                    AppendToBufferAndIncrementPosition(extraLabel.Prometheus, buffer, ref position);
505

506
                AppendToBufferAndIncrementPosition(Quote, buffer, ref position);
507
            }
508

509
            AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position);
510
        }
511
        else
512
        {
513
            AppendToBufferAndIncrementPosition(Space, buffer, ref position);
514
        }
515

516
        return position;
517
    }
518

519
    private int MeasureIdentifierPartLength(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, byte[]? suffix = null)
520
    {
521
        // We mirror the logic in the Write() call but just measure how many bytes of buffer we need.
522
        var length = 0;
523

524
        length += name.Length;
525

526
        if (suffix != null && suffix.Length > 0)
527
        {
528
            length += Underscore.Length;
529
            length += suffix.Length;
530
        }
531

532
        if (flattenedLabels.Length > 0 || extraLabel.IsNotEmpty)
533
        {
534
            length += LeftBrace.Length;
535
            if (flattenedLabels.Length > 0)
536
            {
537
                length += flattenedLabels.Length;
538
            }
539

540
            // Extra labels go to the end (i.e. they are deepest to inherit from).
541
            if (extraLabel.IsNotEmpty)
542
            {
543
                if (flattenedLabels.Length > 0)
544
                {
545
                    length += Comma.Length;
546
                }
547

548
                length += extraLabel.Name.Length;
549
                length += Equal.Length;
550
                length += Quote.Length;
551

552
                if (_expositionFormat == ExpositionFormat.OpenMetricsText)
553
                    length += extraLabel.OpenMetrics.Length;
554
                else
555
                    length += extraLabel.Prometheus.Length;
556

557
                length += Quote.Length;
558
            }
559

560
            length += RightBraceSpace.Length;
561
        }
562
        else
563
        {
564
            length += Space.Length;
565
        }
566

567
        return length;
568
    }
569

570
    /// <summary>
571
    /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be
572
    /// the same.
573
    /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers
574
    /// </summary>
575
    internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value)
576
    {
577
        if (double.IsPositiveInfinity(value))
578
            return new CanonicalLabel(name, PositiveInfinityBytes, PositiveInfinityBytes);
579

580
        // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd
581
        Span<char> buffer = stackalloc char[32];
582

583
        if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture))
584
            throw new Exception("Failed to encode floating point value as string.");
585

586
        var prometheusChars = buffer[0..charsWritten];
587

588
        var prometheusByteCount = PrometheusConstants.ExportEncoding.GetByteCount(prometheusChars);
589
        var prometheusBytes = new byte[prometheusByteCount];
590

591
        if (PrometheusConstants.ExportEncoding.GetBytes(prometheusChars, prometheusBytes) != prometheusByteCount)
592
            throw new Exception("Internal error: counting the same bytes twice got us a different value.");
593

594
        var openMetricsByteCount = prometheusByteCount;
595
        byte[] openMetricsBytes;
596

597
        // Identify whether the written characters are expressed as floating-point, by checking for presence of the 'e' or '.' characters.
598
        if (prometheusChars.IndexOfAny(DotEChar) == -1)
599
        {
600
            // Prometheus defaults to integer-formatting without a decimal point, if possible.
601
            // OpenMetrics requires labels containing numeric values to be expressed in floating point format.
602
            // If all we find is an integer, we add a ".0" to the end to make it a floating point value.
603
            openMetricsByteCount += 2;
604

605
            openMetricsBytes = new byte[openMetricsByteCount];
606
            Array.Copy(prometheusBytes, openMetricsBytes, prometheusByteCount);
607

608
            DotZero.CopyTo(openMetricsBytes.AsSpan(prometheusByteCount));
609
        }
610
        else
611
        {
612
            // It is already a floating-point value in Prometheus representation - reuse same bytes for OpenMetrics.
613
            openMetricsBytes = prometheusBytes;
614
        }
615

616
        return new CanonicalLabel(name, prometheusBytes, openMetricsBytes);
617
    }
618
}
619
#endif

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.