prometheus-net

Форк
0
/
TextSerializer.NetStandardFx.cs 
317 строк · 14.5 Кб
1
#if !NET
2
using System.Globalization;
3

4
namespace Prometheus;
5

6
/// <remarks>
7
/// Does NOT take ownership of the stream - caller remains the boss.
8
/// </remarks>
9
internal sealed class TextSerializer : IMetricsSerializer
10
{
11
    internal static readonly byte[] NewLine = [(byte)'\n'];
12
    internal static readonly byte[] Quote = [(byte)'"'];
13
    internal static readonly byte[] Equal = [(byte)'='];
14
    internal static readonly byte[] Comma = [(byte)','];
15
    internal static readonly byte[] Underscore = [(byte)'_'];
16
    internal static readonly byte[] LeftBrace = [(byte)'{'];
17
    internal static readonly byte[] RightBraceSpace = [(byte)'}', (byte)' '];
18
    internal static readonly byte[] Space = [(byte)' '];
19
    internal static readonly byte[] SpaceHashSpaceLeftBrace = [(byte)' ', (byte)'#', (byte)' ', (byte)'{'];
20
    internal static readonly byte[] PositiveInfinity = "+Inf"u8.ToArray();
21
    internal static readonly byte[] NegativeInfinity = "-Inf"u8.ToArray();
22
    internal static readonly byte[] NotANumber = "NaN"u8.ToArray();
23
    internal static readonly byte[] DotZero = ".0"u8.ToArray();
24
    internal static readonly byte[] FloatPositiveOne = "1.0"u8.ToArray();
25
    internal static readonly byte[] FloatZero = "0.0"u8.ToArray();
26
    internal static readonly byte[] FloatNegativeOne = "-1.0"u8.ToArray();
27
    internal static readonly byte[] IntPositiveOne = "1"u8.ToArray();
28
    internal static readonly byte[] IntZero = "0"u8.ToArray();
29
    internal static readonly byte[] IntNegativeOne = "-1"u8.ToArray();
30
    internal static readonly byte[] EofNewLine = "# EOF\n"u8.ToArray();
31
    internal static readonly byte[] HashHelpSpace = "# HELP "u8.ToArray();
32
    internal static readonly byte[] NewlineHashTypeSpace = "\n# TYPE "u8.ToArray();
33

34
    internal static readonly byte[] Unknown = "unknown"u8.ToArray();
35

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

44
    private static readonly char[] DotEChar = ['.', 'e'];
45

46
    public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText)
47
    {
48
        _expositionFormat = fmt;
49
        _stream = new Lazy<Stream>(() => AddStreamBuffering(stream));
50
    }
51

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

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

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

77
        await _stream.Value.FlushAsync(cancel);
78
    }
79

80
    private readonly Lazy<Stream> _stream;
81

82
    public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type,
83
        byte[] typeBytes, CancellationToken cancel)
84
    {
85
        var nameLen = nameBytes.Length;
86
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter)
87
        {
88
            if (name.EndsWith("_total"))
89
            {
90
                nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix.
91
            }
92
            else
93
            {
94
                typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec
95
            }
96
        }
97

98
        await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel);
99
        await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel);
100
        // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text.
101
        await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel);
102
        if (helpBytes.Length > 0)
103
        {
104
            await _stream.Value.WriteAsync(helpBytes, 0, helpBytes.Length, cancel);
105
        }
106
        await _stream.Value.WriteAsync(NewlineHashTypeSpace, 0, NewlineHashTypeSpace.Length, cancel);
107
        await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel);
108
        await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel);
109
        await _stream.Value.WriteAsync(typeBytes, 0, typeBytes.Length, cancel);
110
        await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel);
111
    }
112

113
    public async ValueTask WriteEnd(CancellationToken cancel)
114
    {
115
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
116
            await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel);
117
    }
118

119
    public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel,
120
        double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel)
121
    {
122
        await WriteIdentifierPartAsync(name, flattenedLabels, canonicalLabel, suffix, cancel);
123

124
        await WriteValue(value, cancel);
125
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid)
126
        {
127
            await WriteExemplarAsync(cancel, exemplar);
128
        }
129

130
        await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel);
131
    }
132

133
    public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel,
134
        long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel)
135
    {
136
        await WriteIdentifierPartAsync(name, flattenedLabels, canonicalLabel, suffix, cancel);
137

138
        await WriteValue(value, cancel);
139
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid)
140
        {
141
            await WriteExemplarAsync(cancel, exemplar);
142
        }
143

144
        await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel);
145
    }
146

147
    private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar)
148
    {
149
        await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel);
150
        for (var i = 0; i < exemplar.Labels!.Length; i++)
151
        {
152
            if (i > 0)
153
                await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel);
154

155
            await WriteLabel(exemplar.Labels![i].KeyBytes, PrometheusConstants.ExemplarEncoding.GetBytes(exemplar.Labels![i].Value), cancel);
156
        }
157

158
        await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel);
159
        await WriteValue(exemplar.Value, cancel);
160
        await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel);
161
        await WriteValue(exemplar.Timestamp, cancel);
162
    }
163

164
    private async Task WriteLabel(byte[] label, byte[] value, CancellationToken cancel)
165
    {
166
        await _stream.Value.WriteAsync(label, 0, label.Length, cancel);
167
        await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel);
168
        await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel);
169
        await _stream.Value.WriteAsync(value, 0, value.Length, cancel);
170
        await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel);
171
    }
172

173
    private async Task WriteValue(double value, CancellationToken cancel)
174
    {
175
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
176
        {
177
            switch (value)
178
            {
179
                case 0:
180
                    await _stream.Value.WriteAsync(FloatZero, 0, FloatZero.Length, cancel);
181
                    return;
182
                case 1:
183
                    await _stream.Value.WriteAsync(FloatPositiveOne, 0, FloatPositiveOne.Length, cancel);
184
                    return;
185
                case -1:
186
                    await _stream.Value.WriteAsync(FloatNegativeOne, 0, FloatNegativeOne.Length, cancel);
187
                    return;
188
                case double.PositiveInfinity:
189
                    await _stream.Value.WriteAsync(PositiveInfinity, 0, PositiveInfinity.Length, cancel);
190
                    return;
191
                case double.NegativeInfinity:
192
                    await _stream.Value.WriteAsync(NegativeInfinity, 0, NegativeInfinity.Length, cancel);
193
                    return;
194
                case double.NaN:
195
                    await _stream.Value.WriteAsync(NotANumber, 0, NotANumber.Length, cancel);
196
                    return;
197
            }
198
        }
199

200
        var valueAsString = value.ToString("g", CultureInfo.InvariantCulture);
201

202
        var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0);
203
        await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel);
204

205
        // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics.
206
        if (_expositionFormat == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */)
207
            await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel);
208
    }
209

210
    private async Task WriteValue(long value, CancellationToken cancel)
211
    {
212
        if (_expositionFormat == ExpositionFormat.OpenMetricsText)
213
        {
214
            switch (value)
215
            {
216
                case 0:
217
                    await _stream.Value.WriteAsync(IntZero, 0, IntZero.Length, cancel);
218
                    return;
219
                case 1:
220
                    await _stream.Value.WriteAsync(IntPositiveOne, 0, IntPositiveOne.Length, cancel);
221
                    return;
222
                case -1:
223
                    await _stream.Value.WriteAsync(IntNegativeOne, 0, IntNegativeOne.Length, cancel);
224
                    return;
225
            }
226
        }
227

228
        var valueAsString = value.ToString("D", CultureInfo.InvariantCulture);
229
        var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0);
230
        await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel);
231
    }
232

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

238
    private readonly ExpositionFormat _expositionFormat;
239

240
    /// <summary>
241
    /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end.
242
    /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"}
243
    /// Note: Terminates with a SPACE
244
    /// </summary>
245
    private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels,
246
        CanonicalLabel canonicalLabel, byte[]? suffix, CancellationToken cancel)
247
    {
248
        await _stream.Value.WriteAsync(name, 0, name.Length, cancel);
249
        if (suffix != null && suffix.Length > 0)
250
        {
251
            await _stream.Value.WriteAsync(Underscore, 0, Underscore.Length, cancel);
252
            await _stream.Value.WriteAsync(suffix, 0, suffix.Length, cancel);
253
        }
254

255
        if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty)
256
        {
257
            await _stream.Value.WriteAsync(LeftBrace, 0, LeftBrace.Length, cancel);
258
            if (flattenedLabels.Length > 0)
259
            {
260
                await _stream.Value.WriteAsync(flattenedLabels, 0, flattenedLabels.Length, cancel);
261
            }
262

263
            // Extra labels go to the end (i.e. they are deepest to inherit from).
264
            if (canonicalLabel.IsNotEmpty)
265
            {
266
                if (flattenedLabels.Length > 0)
267
                {
268
                    await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel);
269
                }
270

271
                await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel);
272
                await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel);
273
                await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel);
274
                if (_expositionFormat == ExpositionFormat.OpenMetricsText)
275
                    await _stream.Value.WriteAsync(
276
                        canonicalLabel.OpenMetrics, 0, canonicalLabel.OpenMetrics.Length, cancel);
277
                else
278
                    await _stream.Value.WriteAsync(
279
                        canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, cancel);
280
                await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel);
281
            }
282

283
            await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel);
284
        }
285
        else
286
        {
287
            await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel);
288
        }
289
    }
290

291
    /// <summary>
292
    /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be
293
    /// the same.
294
    /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers
295
    /// </summary>
296
    internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value)
297
    {
298
        if (double.IsPositiveInfinity(value))
299
            return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity);
300

301
        var valueAsString = value.ToString("g", CultureInfo.InvariantCulture);
302
        var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString);
303

304
        var openMetricsBytes = prometheusBytes;
305

306
        // Identify whether the original value is floating-point, by checking for presence of the 'e' or '.' characters.
307
        if (valueAsString.IndexOfAny(DotEChar) == -1)
308
        {
309
            // OpenMetrics requires labels containing numeric values to be expressed in floating point format.
310
            // If all we find is an integer, we add a ".0" to the end to make it a floating point value.
311
            openMetricsBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0");
312
        }
313

314
        return new CanonicalLabel(name, prometheusBytes, openMetricsBytes);
315
    }
316
}
317
#endif

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

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

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

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