prometheus-net
238 строк · 9.8 Кб
1using System.Collections.Concurrent;2using System.Diagnostics;3using System.Diagnostics.Tracing;4using System.Globalization;5
6namespace Prometheus;7
8/// <summary>
9/// Monitors .NET EventCounters and exposes them as Prometheus metrics.
10/// </summary>
11/// <remarks>
12/// All observed .NET event counters are transformed into Prometheus metrics with translated names.
13/// </remarks>
14public sealed class EventCounterAdapter : IDisposable15{
16public static IDisposable StartListening() => StartListening(EventCounterAdapterOptions.Default);17
18public static IDisposable StartListening(EventCounterAdapterOptions options)19{20// If we are re-registering an adapter with the default options, just pretend and move on.21// The purpose of this code is to avoid double-counting metrics if the adapter is registered twice with the default options.22// This could happen because in 7.0.0 we added automatic registration of the adapters on startup, but the user might still23// have a manual registration active from 6.0.0 days. We do this small thing here to make upgrading less hassle.24if (options == EventCounterAdapterOptions.Default)25{26if (options.Registry.PreventEventCounterAdapterRegistrationWithDefaultOptions)27return new NoopDisposable();28
29options.Registry.PreventEventCounterAdapterRegistrationWithDefaultOptions = true;30}31
32return new EventCounterAdapter(options);33}34
35private EventCounterAdapter(EventCounterAdapterOptions options)36{37_options = options;38_metricFactory = _options.MetricFactory ?? Metrics.WithCustomRegistry(_options.Registry);39
40_eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter.");41
42EventCounterAdapterMemoryWarden.EnsureStarted();43
44_listener = new Listener(ShouldUseEventSource, ConfigureEventSource, options.UpdateInterval, OnEventWritten);45}46
47public void Dispose()48{49// Disposal means we stop listening but we do not remove any published data just to keep things simple.50_listener.Dispose();51}52
53private readonly EventCounterAdapterOptions _options;54private readonly IMetricFactory _metricFactory;55
56private readonly Listener _listener;57
58// We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a gauge.59private readonly Gauge _eventSourcesConnected;60
61private bool ShouldUseEventSource(EventSource source)62{63bool connect = _options.EventSourceFilterPredicate(source.Name);64
65if (connect)66_eventSourcesConnected.Inc();67
68return connect;69}70
71private EventCounterAdapterEventSourceSettings ConfigureEventSource(EventSource source)72{73return _options.EventSourceSettingsProvider(source.Name);74}75
76private const string RateSuffix = "_rate";77
78private void OnEventWritten(EventWrittenEventArgs args)79{80// This deserialization here is pretty gnarly.81// We just skip anything that makes no sense.82
83try84{85if (args.EventName != "EventCounters")86return; // Do not know what it is and do not care.87
88if (args.Payload == null)89return; // What? Whatever.90
91var eventSourceName = args.EventSource.Name;92
93foreach (var item in args.Payload)94{95if (item is not IDictionary<string, object> e)96continue;97
98if (!e.TryGetValue("Name", out var nameWrapper))99continue;100
101var name = nameWrapper as string;102
103if (name == null)104continue; // What? Whatever.105
106if (!e.TryGetValue("DisplayName", out var displayNameWrapper))107continue;108
109var displayName = displayNameWrapper as string ?? "";110
111// If there is a DisplayUnits, prefix it to the help text.112if (e.TryGetValue("DisplayUnits", out var displayUnitsWrapper) && !string.IsNullOrWhiteSpace(displayUnitsWrapper as string))113displayName = $"({(string)displayUnitsWrapper}) {displayName}";114
115var mergedName = $"{eventSourceName}_{name}";116
117var prometheusName = _counterPrometheusName.GetOrAdd(mergedName, PrometheusNameHelpers.TranslateNameToPrometheusName);118
119// The event counter can either be120// 1) an aggregating counter (in which case we use the mean); or121// 2) an incrementing counter (in which case we use the delta).122
123if (e.TryGetValue("Increment", out var increment))124{125// Looks like an incrementing counter.126
127var value = increment as double?;128
129if (value == null)130continue; // What? Whatever.131
132// If the underlying metric is exposing a rate then this can result in some strange terminology like "rate_total".133// We will remove the "rate" from the name to be more understandable - you'll get the rate when you apply the Prometheus rate() function, the raw value is not the rate.134if (prometheusName.EndsWith(RateSuffix))135prometheusName = prometheusName.Remove(prometheusName.Length - RateSuffix.Length);136
137_metricFactory.CreateCounter(prometheusName + "_total", displayName).Inc(value.Value);138}139else if (e.TryGetValue("Mean", out var mean))140{141// Looks like an aggregating counter.142
143var value = mean as double?;144
145if (value == null)146continue; // What? Whatever.147
148_metricFactory.CreateGauge(prometheusName, displayName).Set(value.Value);149}150}151}152catch (Exception ex)153{154// We do not want to throw any exceptions if we fail to handle this event because who knows what it messes up upstream.155Trace.WriteLine($"Failed to parse EventCounter event: {ex.Message}");156}157}158
159// Source+Name -> Name160private readonly ConcurrentDictionary<string, string> _counterPrometheusName = new();161
162private sealed class Listener : EventListener163{164public Listener(165Func<EventSource, bool> shouldUseEventSource,166Func<EventSource, EventCounterAdapterEventSourceSettings> configureEventSosurce,167TimeSpan updateInterval,168Action<EventWrittenEventArgs> onEventWritten)169{170_shouldUseEventSource = shouldUseEventSource;171_configureEventSosurce = configureEventSosurce;172_updateInterval = updateInterval;173_onEventWritten = onEventWritten;174
175foreach (var eventSource in _preRegisteredEventSources)176OnEventSourceCreated(eventSource);177
178_preRegisteredEventSources.Clear();179}180
181private readonly List<EventSource> _preRegisteredEventSources = new List<EventSource>();182
183private readonly Func<EventSource, bool> _shouldUseEventSource;184private readonly Func<EventSource, EventCounterAdapterEventSourceSettings> _configureEventSosurce;185private readonly TimeSpan _updateInterval;186private readonly Action<EventWrittenEventArgs> _onEventWritten;187
188protected override void OnEventSourceCreated(EventSource eventSource)189{190if (_shouldUseEventSource == null)191{192// The way this EventListener thing works is rather strange. Immediately in the base class constructor, before we193// have even had time to wire up our subclass, it starts calling OnEventSourceCreated for all already-existing event sources...194// We just buffer those calls because CALM DOWN SIR!195_preRegisteredEventSources.Add(eventSource);196return;197}198
199if (!_shouldUseEventSource(eventSource))200return;201
202try203{204var options = _configureEventSosurce(eventSource);205
206EnableEvents(eventSource, options.MinimumLevel, options.MatchKeywords, new Dictionary<string, string?>()207{208["EventCounterIntervalSec"] = ((int)Math.Max(1, _updateInterval.TotalSeconds)).ToString(CultureInfo.InvariantCulture),209});210}211catch (Exception ex)212{213// Eat exceptions here to ensure no harm comes of failed enabling.214// The EventCounter infrastructure has proven quite buggy and while it is not certain that this may throw, let's be paranoid.215Trace.WriteLine($"Failed to enable EventCounter listening for {eventSource.Name}: {ex.Message}");216}217}218
219protected override void OnEventWritten(EventWrittenEventArgs eventData)220{221_onEventWritten(eventData);222}223}224
225/// <summary>226/// By default we enable event sources that start with any of these strings. This is a manually curated list to try enable some useful ones227/// without just enabling everything under the sky (because .NET has no way to say "enable only the event counters", you have to enable all diagnostic events).228/// </summary>229private static readonly IReadOnlyList<string> DefaultEventSourcePrefixes = new[]230{231"System.Runtime",232"Microsoft-AspNetCore",233"Microsoft.AspNetCore",234"System.Net"235};236
237public static readonly Func<string, bool> DefaultEventSourceFilterPredicate = name => DefaultEventSourcePrefixes.Any(x => name.StartsWith(x, StringComparison.Ordinal));238}
239