prometheus-net
304 строки · 10.8 Кб
1using System.Diagnostics;
2using System.Runtime.CompilerServices;
3using System.Text;
4using Microsoft.Extensions.ObjectPool;
5
6namespace Prometheus;
7
8/// <summary>
9/// A fully-formed exemplar, describing a set of label name-value pairs.
10///
11/// One-time use only - when you pass an instance to a prometheus-net method, it will take ownership of it.
12///
13/// You should preallocate and cache:
14/// 1. The exemplar keys created via Exemplar.Key().
15/// 2. Exemplar key-value pairs created vvia key.WithValue() or Exemplar.Pair().
16///
17/// From the key-value pairs you can create one-use Exemplar values using Exemplar.From().
18/// You can clone Exemplar instances using Exemplar.Clone() - each clone can only be used once!
19/// </summary>
20public sealed class Exemplar
21{
22/// <summary>
23/// Indicates that no exemplar is to be recorded for a given observation.
24/// </summary>
25public static readonly Exemplar None = new(0);
26
27/// <summary>
28/// An exemplar label key. For optimal performance, create it once and reuse it forever.
29/// </summary>
30public readonly struct LabelKey
31{
32internal LabelKey(byte[] key)
33{
34Bytes = key;
35}
36
37// We only support ASCII here, so rune count always matches byte count.
38internal int RuneCount => Bytes.Length;
39
40internal byte[] Bytes { get; }
41
42/// <summary>
43/// Create a LabelPair once a value is available
44///
45/// The string is expected to only contain runes in the ASCII range, runes outside the ASCII range will get replaced
46/// with placeholders. This constraint may be relaxed with future versions.
47/// </summary>
48[MethodImpl(MethodImplOptions.AggressiveInlining)]
49public LabelPair WithValue(string value)
50{
51static bool IsAscii(ReadOnlySpan<char> chars)
52{
53for (var i = 0; i < chars.Length; i++)
54if (chars[i] > 127)
55return false;
56
57return true;
58}
59
60if (!IsAscii(value.AsSpan()))
61{
62// We believe that approximately 100% of use cases only consist of ASCII characters.
63// That being said, we do not want to throw an exception here as the value may be coming from external sources
64// that calling code has little control over. Therefore, we just replace such characters with placeholders.
65// This matches the default behavior of Encoding.ASCII.GetBytes() - it replaces non-ASCII characters with '?'.
66// As this is a highly theoretical case, we do an inefficient conversion here using the built-in encoder.
67value = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(value));
68}
69
70return new LabelPair(Bytes, value);
71}
72}
73
74/// <summary>
75/// A single exemplar label pair in a form suitable for efficient serialization.
76/// If you wish to reuse the same key-value pair, you should reuse this object as much as possible.
77/// </summary>
78public readonly struct LabelPair
79{
80internal LabelPair(byte[] keyBytes, string value)
81{
82KeyBytes = keyBytes;
83Value = value;
84}
85
86internal int RuneCount => KeyBytes.Length + Value.Length;
87internal byte[] KeyBytes { get; }
88
89// We keep the value as a string because it typically starts out its life as a string
90// and we want to avoid paying the cost of converting it to a byte array until we serialize it.
91// If we record many exemplars then we may, in fact, never serialize most of them because they get replaced.
92internal string Value { get; }
93}
94
95/// <summary>
96/// Return an exemplar label key, this may be curried with a value to produce a LabelPair.
97/// Reuse this for optimal performance.
98/// </summary>
99public static LabelKey Key(string key)
100{
101if (string.IsNullOrWhiteSpace(key))
102throw new ArgumentException("empty key", nameof(key));
103
104Collector.ValidateLabelName(key);
105
106var asciiBytes = Encoding.ASCII.GetBytes(key);
107return new LabelKey(asciiBytes);
108}
109
110/// <summary>
111/// Pair constructs a LabelPair, it is advisable to memoize a "Key" (eg: "traceID") and then to derive "LabelPair"s
112/// from these. You may (should) reuse a LabelPair for recording multiple observations that use the same exemplar.
113/// </summary>
114public static LabelPair Pair(string key, string value)
115{
116return Key(key).WithValue(value);
117}
118
119[MethodImpl(MethodImplOptions.AggressiveInlining)]
120public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5, in LabelPair labelPair6)
121{
122var exemplar = Exemplar.AllocateFromPool(length: 6);
123exemplar.LabelPair1 = labelPair1;
124exemplar.LabelPair2 = labelPair2;
125exemplar.LabelPair3 = labelPair3;
126exemplar.LabelPair4 = labelPair4;
127exemplar.LabelPair5 = labelPair5;
128exemplar.LabelPair6 = labelPair6;
129
130return exemplar;
131}
132
133[MethodImpl(MethodImplOptions.AggressiveInlining)]
134public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5)
135{
136var exemplar = Exemplar.AllocateFromPool(length: 5);
137exemplar.LabelPair1 = labelPair1;
138exemplar.LabelPair2 = labelPair2;
139exemplar.LabelPair3 = labelPair3;
140exemplar.LabelPair4 = labelPair4;
141exemplar.LabelPair5 = labelPair5;
142
143return exemplar;
144}
145
146[MethodImpl(MethodImplOptions.AggressiveInlining)]
147public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4)
148{
149var exemplar = Exemplar.AllocateFromPool(length: 4);
150exemplar.LabelPair1 = labelPair1;
151exemplar.LabelPair2 = labelPair2;
152exemplar.LabelPair3 = labelPair3;
153exemplar.LabelPair4 = labelPair4;
154
155return exemplar;
156}
157
158[MethodImpl(MethodImplOptions.AggressiveInlining)]
159public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3)
160{
161var exemplar = Exemplar.AllocateFromPool(length: 3);
162exemplar.LabelPair1 = labelPair1;
163exemplar.LabelPair2 = labelPair2;
164exemplar.LabelPair3 = labelPair3;
165
166return exemplar;
167}
168
169[MethodImpl(MethodImplOptions.AggressiveInlining)]
170public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2)
171{
172var exemplar = Exemplar.AllocateFromPool(length: 2);
173exemplar.LabelPair1 = labelPair1;
174exemplar.LabelPair2 = labelPair2;
175
176return exemplar;
177}
178
179[MethodImpl(MethodImplOptions.AggressiveInlining)]
180public static Exemplar From(in LabelPair labelPair1)
181{
182var exemplar = Exemplar.AllocateFromPool(length: 1);
183exemplar.LabelPair1 = labelPair1;
184
185return exemplar;
186}
187
188internal ref LabelPair this[int index]
189{
190[MethodImpl(MethodImplOptions.AggressiveInlining)]
191get
192{
193if (index == 0) return ref LabelPair1;
194if (index == 1) return ref LabelPair2;
195if (index == 2) return ref LabelPair3;
196if (index == 3) return ref LabelPair4;
197if (index == 4) return ref LabelPair5;
198if (index == 5) return ref LabelPair6;
199throw new ArgumentOutOfRangeException(nameof(index));
200}
201}
202
203// Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/
204private static readonly LabelKey DefaultTraceIdKey = Key("trace_id");
205private static readonly LabelKey DefaultSpanIdKey = Key("span_id");
206
207public static Exemplar FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey);
208
209public static Exemplar FromTraceContext(in LabelKey traceIdKey, in LabelKey spanIdKey)
210{
211#if NET6_0_OR_GREATER
212var activity = Activity.Current;
213if (activity != null)
214{
215// These values already exist as strings inside the Activity logic, so there is no string allocation happening here.
216var traceIdLabel = traceIdKey.WithValue(activity.TraceId.ToString());
217var spanIdLabel = spanIdKey.WithValue(activity.SpanId.ToString());
218
219return From(traceIdLabel, spanIdLabel);
220}
221#endif
222
223// Trace context based exemplars are only supported in .NET Core, not .NET Framework.
224return None;
225}
226
227public Exemplar()
228{
229}
230
231private Exemplar(int length)
232{
233Length = length;
234}
235
236[MethodImpl(MethodImplOptions.AggressiveInlining)]
237internal void Update(int length)
238{
239Length = length;
240Interlocked.Exchange(ref _consumed, IsNotConsumed);
241}
242
243/// <summary>
244/// Number of label pairs in use.
245/// </summary>
246internal int Length { get; private set; }
247
248internal LabelPair LabelPair1;
249internal LabelPair LabelPair2;
250internal LabelPair LabelPair3;
251internal LabelPair LabelPair4;
252internal LabelPair LabelPair5;
253internal LabelPair LabelPair6;
254
255private static readonly ObjectPool<Exemplar> ExemplarPool = ObjectPool.Create<Exemplar>();
256
257[MethodImpl(MethodImplOptions.AggressiveInlining)]
258internal static Exemplar AllocateFromPool(int length)
259{
260var instance = ExemplarPool.Get();
261instance.Update(length);
262return instance;
263}
264
265[MethodImpl(MethodImplOptions.AggressiveInlining)]
266internal void ReturnToPoolIfNotEmpty()
267{
268if (Length == 0)
269return; // Only the None instance can have a length of 0.
270
271Length = 0;
272
273ExemplarPool.Return(this);
274}
275
276private long _consumed;
277
278private const long IsConsumed = 1;
279private const long IsNotConsumed = 0;
280
281internal void MarkAsConsumed()
282{
283if (Interlocked.Exchange(ref _consumed, IsConsumed) == IsConsumed)
284throw new InvalidOperationException($"An instance of {nameof(Exemplar)} was reused. You must obtain a new instance via Exemplar.From() or Exemplar.Clone() for each metric value observation.");
285}
286
287/// <summary>
288/// Clones the exemplar so it can be reused - each copy can only be used once!
289/// </summary>
290public Exemplar Clone()
291{
292if (Interlocked.Read(ref _consumed) == IsConsumed)
293throw new InvalidOperationException($"An instance of {nameof(Exemplar)} cannot be cloned after it has already been used.");
294
295var clone = AllocateFromPool(Length);
296clone.LabelPair1 = LabelPair1;
297clone.LabelPair2 = LabelPair2;
298clone.LabelPair3 = LabelPair3;
299clone.LabelPair4 = LabelPair4;
300clone.LabelPair5 = LabelPair5;
301clone.LabelPair6 = LabelPair6;
302return clone;
303}
304}