src/Elastic.Apm/Metrics/MetricsProvider/GcMetricsProvider.cs (202 lines of code) (raw):

// Licensed to Elasticsearch B.V under // one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Threading; using Elastic.Apm.Api; using Elastic.Apm.Helpers; using Elastic.Apm.Logging; // ReSharper disable AccessToDisposedClosure namespace Elastic.Apm.Metrics.MetricsProvider { /// <summary> /// A metrics provider that collects GC metrics. /// On .NET Core it collects metrics through EventSource, /// </summary> internal class GcMetricsProvider : IMetricsProvider, IDisposable { internal const string GcCountName = "clr.gc.count"; internal const string GcGen0SizeName = "clr.gc.gen0size"; internal const string GcGen1SizeName = "clr.gc.gen1size"; internal const string GcGen2SizeName = "clr.gc.gen2size"; internal const string GcGen3SizeName = "clr.gc.gen3size"; internal const string GcTimeName = "clr.gc.time"; private readonly bool _collectGcCount; private readonly bool _collectGcGen0Size; private readonly bool _collectGcGen1Size; private readonly bool _collectGcGen2Size; private readonly bool _collectGcGen3Size; private readonly bool _collectGcTime; private readonly GcEventListener _eventListener; private readonly object _lock = new object(); private readonly IApmLogger _logger; private uint _gcCount; private long _gcTimeInTicks; private ulong _gen0Size; private ulong _gen1Size; private ulong _gen2Size; private ulong _gen3Size; private volatile bool _isMetricAlreadyCaptured; private readonly bool _isEnabled; public GcMetricsProvider(IApmLogger logger, IReadOnlyList<WildcardMatcher> disabledMetrics) { _collectGcCount = !WildcardMatcher.IsAnyMatch(disabledMetrics, GcCountName); _collectGcTime = !WildcardMatcher.IsAnyMatch(disabledMetrics, GcTimeName); _collectGcGen0Size = !WildcardMatcher.IsAnyMatch(disabledMetrics, GcGen0SizeName); _collectGcGen1Size = !WildcardMatcher.IsAnyMatch(disabledMetrics, GcGen1SizeName); _collectGcGen2Size = !WildcardMatcher.IsAnyMatch(disabledMetrics, GcGen2SizeName); _collectGcGen3Size = !WildcardMatcher.IsAnyMatch(disabledMetrics, GcGen3SizeName); _isEnabled = _collectGcCount || _collectGcTime || _collectGcGen0Size || _collectGcGen1Size || _collectGcGen2Size || _collectGcGen3Size; if (!IsEnabled(disabledMetrics)) return; _logger = logger.Scoped(DbgName); if (!PlatformDetection.IsDotNetCore && !PlatformDetection.IsDotNet) { _logger.Info()?.Log("GC metrics are only available on .NET Core, disabling metric collection"); _isEnabled = false; return; } _eventListener = new GcEventListener(this, logger); } public int ConsecutiveNumberOfFailedReads { get; set; } public string DbgName => nameof(GcMetricsProvider); public bool IsMetricAlreadyCaptured { get { lock (_lock) return _isMetricAlreadyCaptured; } } public bool IsEnabled(IReadOnlyList<WildcardMatcher> disabledMetrics) => _isEnabled; public IEnumerable<MetricSet> GetSamples() { var gcTimeInMs = Interlocked.Exchange(ref _gcTimeInTicks, 0) / 10_000.0; if (_gcCount != 0 || _gen0Size != 0 || _gen2Size != 0 || _gen3Size != 0 || gcTimeInMs > 0) { var samples = new List<MetricSample>(6); if (_collectGcCount) samples.Add(new MetricSample(GcCountName, _gcCount)); if (_collectGcTime) samples.Add(new MetricSample(GcTimeName, Math.Round(gcTimeInMs, 6))); if (_collectGcGen0Size) samples.Add(new MetricSample(GcGen0SizeName, _gen0Size)); if (_collectGcGen1Size) samples.Add(new MetricSample(GcGen1SizeName, _gen1Size)); if (_collectGcGen2Size) samples.Add(new MetricSample(GcGen2SizeName, _gen2Size)); if (_collectGcGen3Size) samples.Add(new MetricSample(GcGen3SizeName, _gen3Size)); _logger.Trace() ?.Log( "Collected gc metrics values: gcCount: {gcCount}, gen0Size: {gen0Size}, gen1Size: {gen1Size}, gen2Size: {gen2Size}, gen1Size: {gen3Size}, gcTime: {gcTime}", _gcCount, _gen0Size, _gen1Size, _gen2Size, _gen3Size, gcTimeInMs); return new List<MetricSet> { new(TimeUtils.TimestampNow(), samples) }; } return null; } public void Dispose() => _eventListener?.Dispose(); /// <summary> /// An event listener that collects the GC stats /// </summary> private class GcEventListener : EventListener { private static readonly int keywordGC = 1; private readonly GcMetricsProvider _gcMetricsProvider; private readonly IApmLogger _logger; public GcEventListener(GcMetricsProvider gcMetricsProvider, IApmLogger logger) { _gcMetricsProvider = gcMetricsProvider ?? throw new Exception("gcMetricsProvider is null"); _logger = logger.Scoped(nameof(GcEventListener)); _logger.Trace()?.Log("Initialize GcEventListener to collect GC metrics"); } private EventSource _eventSourceDotNet; private long _gcStartTime; protected override void OnEventSourceCreated(EventSource eventSource) { try { if (!eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime")) return; EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)keywordGC); _eventSourceDotNet = eventSource; _logger?.Trace()?.Log("Microsoft-Windows-DotNETRuntime enabled"); } catch (Exception e) { _logger?.Warning()?.LogException(e, "EnableEvents failed - no GC metrics will be collected"); } } protected override void OnEventWritten(EventWrittenEventArgs eventData) { // Collect heap sizes if (eventData.EventName.Contains("GCHeapStats_V1")) { _logger?.Trace()?.Log("OnEventWritten with GCHeapStats_V1"); SetValue("GenerationSize0", ref _gcMetricsProvider._gen0Size); SetValue("GenerationSize1", ref _gcMetricsProvider._gen1Size); SetValue("GenerationSize2", ref _gcMetricsProvider._gen2Size); SetValue("GenerationSize3", ref _gcMetricsProvider._gen3Size); if (!_gcMetricsProvider._isMetricAlreadyCaptured) { lock (_gcMetricsProvider._lock) _gcMetricsProvider._isMetricAlreadyCaptured = true; } } if (eventData.EventName.Contains("GCHeapStats_V2")) { _logger?.Trace()?.Log("OnEventWritten with GCHeapStats_V2"); SetValue("GenerationSize0", ref _gcMetricsProvider._gen0Size); SetValue("GenerationSize1", ref _gcMetricsProvider._gen1Size); SetValue("GenerationSize2", ref _gcMetricsProvider._gen2Size); SetValue("GenerationSize3", ref _gcMetricsProvider._gen3Size); if (!_gcMetricsProvider._isMetricAlreadyCaptured) { lock (_gcMetricsProvider._lock) _gcMetricsProvider._isMetricAlreadyCaptured = true; } } if (eventData.EventName.Contains("GCStart")) Interlocked.Exchange(ref _gcStartTime, DateTime.UtcNow.Ticks); // Collect GC count and time if (eventData.EventName.Contains("GCEnd")) { if (!_gcMetricsProvider._isMetricAlreadyCaptured) { lock (_gcMetricsProvider._lock) _gcMetricsProvider._isMetricAlreadyCaptured = true; } _logger?.Trace()?.Log("OnEventWritten with GCEnd"); var durationInTicks = DateTime.UtcNow.Ticks - Interlocked.Read(ref _gcStartTime); Interlocked.Exchange(ref _gcMetricsProvider._gcTimeInTicks, Interlocked.Read(ref _gcMetricsProvider._gcTimeInTicks) + durationInTicks); var indexOfCount = IndexOf("Count"); if (indexOfCount < 0) return; var gcCount = eventData.Payload[indexOfCount]; if (!(gcCount is uint gcCountInt)) return; _gcMetricsProvider._gcCount = gcCountInt; } void SetValue(string name, ref ulong value) { var gen0SizeIndex = IndexOf(name); if (gen0SizeIndex < 0) return; var gen0Size = eventData.Payload[gen0SizeIndex]; if (gen0Size is ulong gen0SizeLong) value = gen0SizeLong; } int IndexOf(string name) { return eventData.PayloadNames.IndexOf(name); } } public override void Dispose() { try { if (_eventSourceDotNet != null) { _logger.Trace()?.Log("disposing {classname}", nameof(GcEventListener)); DisableEvents(_eventSourceDotNet); _eventSourceDotNet = null; // calling _eventSourceDotNet.Dispose makes it impossible to re-enable the eventsource, so if we call _eventSourceDotNet.Dispose() // all tests will fail after Dispose() } } catch (Exception e) { _logger.Warning()?.LogException(e, "Disposing {classname} failed", nameof(GcEventListener)); } } } } }