From fa95cb7ac68cbe9a889b1dff72c8e535a5b13dd8 Mon Sep 17 00:00:00 2001 From: kkeirstead <85592574+kkeirstead@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:11:49 -0700 Subject: [PATCH] Dotnet Counters + Dotnet Monitor Unification (#4254) --- .../Counters/CounterPayload.cs | 181 +++-- .../Counters/CounterPayloadExtensions.cs | 21 - .../Counters/ICounterPayload.cs | 6 +- .../Counters/MetricsPipeline.cs | 11 +- .../Counters/TraceEventExtensions.cs | 151 ++-- .../DiagnosticsEventPipeProcessor.cs | 4 +- .../EventPipeStreamProvider.cs | 13 +- .../EventSourcePipeline.cs | 2 +- .../EventSourcePipelineSettings.cs | 2 + ...ft.Diagnostics.Monitoring.EventPipe.csproj | 1 + .../Trace/EventTracePipeline.cs | 2 +- .../EventCounter/EventCounterTrigger.cs | 5 +- .../SystemDiagnosticsMetricsTrigger.cs | 7 +- .../SystemDiagnosticsMetricsTriggerImpl.cs | 10 +- .../Microsoft.Diagnostics.Monitoring.csproj | 2 + src/Tools/dotnet-counters/CounterMonitor.cs | 704 +++++------------- src/Tools/dotnet-counters/CounterPayload.cs | 63 -- .../CounterPayloadExtensions.cs | 28 + .../dotnet-counters/Exporters/CSVExporter.cs | 9 +- .../Exporters/ConsoleWriter.cs | 13 +- .../Exporters/ICounterRenderer.cs | 4 +- .../dotnet-counters/Exporters/JSONExporter.cs | 9 +- src/Tools/dotnet-counters/Program.cs | 4 +- .../EventCounterPipelineUnitTests.cs | 6 +- .../EventCounterTriggerTests.cs | 3 +- src/tests/dotnet-counters/CSVExporterTests.cs | 9 +- .../dotnet-counters/JSONExporterTests.cs | 12 +- 27 files changed, 535 insertions(+), 747 deletions(-) delete mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayloadExtensions.cs delete mode 100644 src/Tools/dotnet-counters/CounterPayload.cs create mode 100644 src/Tools/dotnet-counters/CounterPayloadExtensions.cs diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs index 99e303008..850949b08 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs @@ -7,12 +7,9 @@ using System.Linq; namespace Microsoft.Diagnostics.Monitoring.EventPipe { - /// - /// TODO This is currently a duplication of the src\Tools\dotnet-counters\CounterPayload.cs stack. The two will be unified in a separate change. - /// - internal class CounterPayload : ICounterPayload + internal abstract class CounterPayload : ICounterPayload { - public CounterPayload(DateTime timestamp, + protected CounterPayload(DateTime timestamp, string provider, string name, string displayName, @@ -20,7 +17,9 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe double value, CounterType counterType, float interval, - string metadata) + int series, + string metadata, + EventType eventType) { Timestamp = timestamp; Name = name; @@ -30,30 +29,11 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe CounterType = counterType; Provider = provider; Interval = interval; + Series = series; Metadata = metadata; - EventType = EventType.Gauge; - } - - // Copied from dotnet-counters - public CounterPayload(string providerName, - string name, - string metadata, - double value, - DateTime timestamp, - string type, - EventType eventType) - { - Provider = providerName; - Name = name; - Metadata = metadata; - Value = value; - Timestamp = timestamp; - CounterType = (CounterType)Enum.Parse(typeof(CounterType), type); EventType = eventType; } - public string Namespace { get; } - public string Name { get; } public string DisplayName { get; protected set; } @@ -73,12 +53,50 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe public string Metadata { get; } public EventType EventType { get; set; } + + public virtual bool IsMeter => false; + + public int Series { get; } + } + + internal sealed class EventCounterPayload : CounterPayload + { + public EventCounterPayload(DateTime timestamp, + string provider, + string name, + string displayName, + string unit, + double value, + CounterType counterType, + float interval, + int series, + string metadata) : base(timestamp, provider, name, displayName, unit, value, counterType, interval, series, metadata, EventType.Gauge) + { + } + } + + internal abstract class MeterPayload : CounterPayload + { + protected MeterPayload(DateTime timestamp, + string provider, + string name, + string displayName, + string unit, + double value, + CounterType counterType, + string metadata, + EventType eventType) + : base(timestamp, provider, name, displayName, unit, value, counterType, 0.0f, 0, metadata, eventType) + { + } + + public override bool IsMeter => true; } - internal class GaugePayload : CounterPayload + internal sealed class GaugePayload : MeterPayload { public GaugePayload(string providerName, string name, string displayName, string displayUnits, string metadata, double value, DateTime timestamp) : - base(providerName, name, metadata, value, timestamp, "Metric", EventType.Gauge) + base(timestamp, providerName, name, displayName, displayUnits, value, CounterType.Metric, metadata, EventType.Gauge) { // In case these properties are not provided, set them to appropriate values. string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; @@ -86,10 +104,10 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } - internal class UpDownCounterPayload : CounterPayload + internal class UpDownCounterPayload : MeterPayload { public UpDownCounterPayload(string providerName, string name, string displayName, string displayUnits, string metadata, double value, DateTime timestamp) : - base(providerName, name, metadata, value, timestamp, "Metric", EventType.UpDownCounter) + base(timestamp, providerName, name, displayName, displayUnits, value, CounterType.Metric, metadata, EventType.UpDownCounter) { // In case these properties are not provided, set them to appropriate values. string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; @@ -97,19 +115,26 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } - internal class CounterEndedPayload : CounterPayload + internal sealed class BeginInstrumentReportingPayload : MeterPayload { - public CounterEndedPayload(string providerName, string name, DateTime timestamp) - : base(providerName, name, null, 0.0, timestamp, "Metric", EventType.CounterEnded) + public BeginInstrumentReportingPayload(string providerName, string name, DateTime timestamp) + : base(timestamp, providerName, name, string.Empty, string.Empty, 0.0, CounterType.Metric, null, EventType.BeginInstrumentReporting) { + } + } + internal sealed class CounterEndedPayload : MeterPayload + { + public CounterEndedPayload(string providerName, string name, DateTime timestamp) + : base(timestamp, providerName, name, string.Empty, string.Empty, 0.0, CounterType.Metric, null, EventType.CounterEnded) + { } } - internal class RatePayload : CounterPayload + internal sealed class RatePayload : MeterPayload { public RatePayload(string providerName, string name, string displayName, string displayUnits, string metadata, double value, double intervalSecs, DateTime timestamp) : - base(providerName, name, metadata, value, timestamp, "Rate", EventType.Rate) + base(timestamp, providerName, name, displayName, displayUnits, value, CounterType.Rate, metadata, EventType.Rate) { // In case these properties are not provided, set them to appropriate values. string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; @@ -119,35 +144,46 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } - internal class PercentilePayload : CounterPayload + internal record struct Quantile(double Percentage, double Value); + + internal sealed class PercentilePayload : MeterPayload { - public PercentilePayload(string providerName, string name, string displayName, string displayUnits, string metadata, IEnumerable quantiles, DateTime timestamp) : - base(providerName, name, metadata, 0.0, timestamp, "Metric", EventType.Histogram) + public PercentilePayload(string providerName, string name, string displayName, string displayUnits, string metadata, double value, DateTime timestamp) : + base(timestamp, providerName, name, displayName, displayUnits, value, CounterType.Metric, metadata, EventType.Histogram) { // In case these properties are not provided, set them to appropriate values. string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; DisplayName = !string.IsNullOrEmpty(displayUnits) ? $"{counterName} ({displayUnits})" : counterName; - Quantiles = quantiles.ToArray(); } - - public Quantile[] Quantiles { get; } } - internal record struct Quantile(double Percentage, double Value); - - internal class ErrorPayload : CounterPayload + // Dotnet-monitor and dotnet-counters previously had incompatible PercentilePayload implementations before being unified - + // Dotnet-monitor created a single payload that contained all of the quantiles to keep them together, whereas + // dotnet-counters created a separate payload for each quantile (multiple payloads per TraceEvent). + // AggregatePercentilePayload allows dotnet-monitor to construct a PercentilePayload for individual quantiles + // like dotnet-counters, while still keeping the quantiles together as a unit. + internal sealed class AggregatePercentilePayload : MeterPayload { - public ErrorPayload(string errorMessage) : this(errorMessage, DateTime.UtcNow) + public AggregatePercentilePayload(string providerName, string name, string displayName, string displayUnits, string metadata, IEnumerable quantiles, DateTime timestamp) : + base(timestamp, providerName, name, displayName, displayUnits, 0.0, CounterType.Metric, metadata, EventType.Histogram) { + //string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; + //DisplayName = !string.IsNullOrEmpty(displayUnits) ? $"{counterName} ({displayUnits})" : counterName; + Quantiles = quantiles.ToArray(); } - public ErrorPayload(string errorMessage, DateTime timestamp) : - base(string.Empty, string.Empty, null, 0.0, timestamp, "Metric", EventType.Error) + public Quantile[] Quantiles { get; } + } + + internal sealed class ErrorPayload : MeterPayload + { + public ErrorPayload(string errorMessage, DateTime timestamp, EventType eventType) + : base(timestamp, string.Empty, string.Empty, string.Empty, string.Empty, 0.0, CounterType.Metric, null, eventType) { ErrorMessage = errorMessage; } - public string ErrorMessage { get; private set; } + public string ErrorMessage { get; } } internal enum EventType : int @@ -156,7 +192,52 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe Gauge, Histogram, UpDownCounter, - Error, - CounterEnded + BeginInstrumentReporting, + CounterEnded, + HistogramLimitError, + TimeSeriesLimitError, + ErrorTargetProcess, + MultipleSessionsNotSupportedError, + MultipleSessionsConfiguredIncorrectlyError, + ObservableInstrumentCallbackError + } + + internal static class EventTypeExtensions + { + public static bool IsValuePublishedEvent(this EventType eventType) + { + return eventType is EventType.Gauge + || eventType is EventType.Rate + || eventType is EventType.Histogram + || eventType is EventType.UpDownCounter; + } + + public static bool IsError(this EventType eventType) + { + return eventType is EventType.HistogramLimitError + || eventType is EventType.TimeSeriesLimitError + || eventType is EventType.ErrorTargetProcess + || eventType is EventType.MultipleSessionsNotSupportedError + || eventType is EventType.MultipleSessionsConfiguredIncorrectlyError + || eventType is EventType.ObservableInstrumentCallbackError; + } + + public static bool IsNonFatalError(this EventType eventType) + { + return IsError(eventType) + && !IsTracingError(eventType) + && !IsSessionStartupError(eventType); + } + + public static bool IsTracingError(this EventType eventType) + { + return eventType is EventType.ErrorTargetProcess; + } + + public static bool IsSessionStartupError(this EventType eventType) + { + return eventType is EventType.MultipleSessionsNotSupportedError + || eventType is EventType.MultipleSessionsConfiguredIncorrectlyError; + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayloadExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayloadExtensions.cs deleted file mode 100644 index 12d4b862e..000000000 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayloadExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Diagnostics.Monitoring.EventPipe -{ - internal static class CounterPayloadExtensions - { - public static string GetDisplay(this ICounterPayload counterPayload) - { - if (counterPayload.CounterType == CounterType.Rate) - { - return $"{counterPayload.DisplayName} ({counterPayload.Unit} / {counterPayload.Interval} sec)"; - } - if (!string.IsNullOrEmpty(counterPayload.Unit)) - { - return $"{counterPayload.DisplayName} ({counterPayload.Unit})"; - } - return $"{counterPayload.DisplayName}"; - } - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs index 4e3ce06f7..4cc5fdb04 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs @@ -41,6 +41,10 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe /// string Metadata { get; } - EventType EventType { get; set; } + EventType EventType { get; } + + bool IsMeter { get; } + + int Series { get; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs index 1c3a29ab8..9f8f34c15 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs @@ -17,6 +17,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe private readonly CounterFilter _filter; private string _clientId; private string _sessionId; + private CounterConfiguration _counterConfiguration; public MetricsPipeline(DiagnosticsClient client, MetricsPipelineSettings settings, @@ -51,6 +52,14 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe _clientId = config.ClientId; _sessionId = config.SessionId; + _counterConfiguration = new CounterConfiguration(_filter) + { + SessionId = _sessionId, + ClientId = _clientId, + MaxHistograms = Settings.MaxHistograms, + MaxTimeseries = Settings.MaxTimeSeries + }; + return config; } @@ -61,7 +70,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe eventSource.Dynamic.All += traceEvent => { try { - if (traceEvent.TryGetCounterPayload(_filter, _sessionId, _clientId, out ICounterPayload counterPayload)) + if (traceEvent.TryGetCounterPayload(_counterConfiguration, out ICounterPayload counterPayload)) { ExecuteCounterLoggerAction((metricLogger) => metricLogger.Log(counterPayload)); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs index 942a7ebdd..06472cf3f 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs @@ -9,11 +9,29 @@ using Microsoft.Diagnostics.Tracing; namespace Microsoft.Diagnostics.Monitoring.EventPipe { + internal class CounterConfiguration + { + public CounterConfiguration(CounterFilter filter) + { + CounterFilter = filter ?? throw new ArgumentNullException(nameof(filter)); + } + + public CounterFilter CounterFilter { get; } + + public string SessionId { get; set; } + + public string ClientId { get; set; } + + public int MaxHistograms { get; set; } + + public int MaxTimeseries { get; set; } + } + internal static class TraceEventExtensions { private static HashSet inactiveSharedSessions = new(StringComparer.OrdinalIgnoreCase); - public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, string sessionId, string clientId, out ICounterPayload payload) + public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterConfiguration counterConfiguration, out ICounterPayload payload) { payload = null; @@ -27,12 +45,12 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string counterName = payloadFields["Name"].ToString(); string metadata = payloadFields["Metadata"].ToString(); - + int seriesValue = GetInterval(series); //CONSIDER //Concurrent counter sessions do not each get a separate interval. Instead the payload //for _all_ the counters changes the Series to be the lowest specified interval, on a per provider basis. //Currently the CounterFilter will remove any data whose Series doesn't match the requested interval. - if (!filter.IsIncluded(traceEvent.ProviderName, counterName, GetInterval(series))) + if (!counterConfiguration.CounterFilter.IsIncluded(traceEvent.ProviderName, counterName, seriesValue)) { return false; } @@ -61,7 +79,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe // Note that dimensional data such as pod and namespace are automatically added in prometheus and azure monitor scenarios. // We no longer added it here. - payload = new CounterPayload( + payload = new EventCounterPayload( traceEvent.TimeStamp, traceEvent.ProviderName, counterName, displayName, @@ -69,57 +87,57 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe value, counterType, intervalSec, + seriesValue / 1000, metadata); return true; } - if (clientId != null && !inactiveSharedSessions.Contains(clientId) && MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName.Equals(traceEvent.ProviderName)) + if (counterConfiguration.ClientId != null && !inactiveSharedSessions.Contains(counterConfiguration.ClientId) && MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName.Equals(traceEvent.ProviderName)) { if (traceEvent.EventName == "BeginInstrumentReporting") { - // Do we want to log something for this? - //HandleBeginInstrumentReporting(traceEvent); + HandleBeginInstrumentReporting(traceEvent, counterConfiguration, out payload); } if (traceEvent.EventName == "HistogramValuePublished") { - HandleHistogram(traceEvent, filter, sessionId, out payload); + HandleHistogram(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "GaugeValuePublished") { - HandleGauge(traceEvent, filter, sessionId, out payload); + HandleGauge(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "CounterRateValuePublished") { - HandleCounterRate(traceEvent, filter, sessionId, out payload); + HandleCounterRate(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "UpDownCounterRateValuePublished") { - HandleUpDownCounterValue(traceEvent, filter, sessionId, out payload); + HandleUpDownCounterValue(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "TimeSeriesLimitReached") { - HandleTimeSeriesLimitReached(traceEvent, sessionId, out payload); + HandleTimeSeriesLimitReached(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "HistogramLimitReached") { - HandleHistogramLimitReached(traceEvent, sessionId, out payload); + HandleHistogramLimitReached(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "Error") { - HandleError(traceEvent, sessionId, out payload); + HandleError(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "ObservableInstrumentCallbackError") { - HandleObservableInstrumentCallbackError(traceEvent, sessionId, out payload); + HandleObservableInstrumentCallbackError(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "MultipleSessionsNotSupportedError") { - HandleMultipleSessionsNotSupportedError(traceEvent, sessionId, out payload); + HandleMultipleSessionsNotSupportedError(traceEvent, counterConfiguration, out payload); } else if (traceEvent.EventName == "MultipleSessionsConfiguredIncorrectlyError") { - HandleMultipleSessionsConfiguredIncorrectlyError(traceEvent, clientId, out payload); + HandleMultipleSessionsConfiguredIncorrectlyError(traceEvent, counterConfiguration.ClientId, out payload); } return payload != null; @@ -128,13 +146,13 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe return false; } - private static void HandleGauge(TraceEvent obj, CounterFilter filter, string sessionId, out ICounterPayload payload) + private static void HandleGauge(TraceEvent obj, CounterConfiguration counterConfiguration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); - if (payloadSessionId != sessionId) + if (payloadSessionId != counterConfiguration.SessionId) { return; } @@ -146,7 +164,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string tags = (string)obj.PayloadValue(5); string lastValueText = (string)obj.PayloadValue(6); - if (!filter.IsIncluded(meterName, instrumentName)) + if (!counterConfiguration.CounterFilter.IsIncluded(meterName, instrumentName)) { return; } @@ -164,13 +182,36 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } - private static void HandleCounterRate(TraceEvent traceEvent, CounterFilter filter, string sessionId, out ICounterPayload payload) + private static void HandleBeginInstrumentReporting(TraceEvent traceEvent, CounterConfiguration counterConfiguration, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)traceEvent.PayloadValue(0); + if (payloadSessionId != counterConfiguration.SessionId) + { + return; + } + + string meterName = (string)traceEvent.PayloadValue(1); + //string meterVersion = (string)obj.PayloadValue(2); + string instrumentName = (string)traceEvent.PayloadValue(3); + + if (!counterConfiguration.CounterFilter.IsIncluded(meterName, instrumentName)) + { + return; + } + + + payload = new BeginInstrumentReportingPayload(meterName, instrumentName, traceEvent.TimeStamp); + } + + private static void HandleCounterRate(TraceEvent traceEvent, CounterConfiguration counterConfiguration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)traceEvent.PayloadValue(0); - if (payloadSessionId != sessionId) + if (payloadSessionId != counterConfiguration.SessionId) { return; } @@ -182,14 +223,14 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string tags = (string)traceEvent.PayloadValue(5); string rateText = (string)traceEvent.PayloadValue(6); - if (!filter.IsIncluded(meterName, instrumentName)) + if (!counterConfiguration.CounterFilter.IsIncluded(meterName, instrumentName)) { return; } - if (double.TryParse(rateText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double rate)) + if (double.TryParse(rateText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) { - payload = new RatePayload(meterName, instrumentName, null, unit, tags, rate, filter.DefaultIntervalSeconds, traceEvent.TimeStamp); + payload = new RatePayload(meterName, instrumentName, null, unit, tags, value, counterConfiguration.CounterFilter.DefaultIntervalSeconds, traceEvent.TimeStamp); } else { @@ -200,13 +241,13 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } - private static void HandleUpDownCounterValue(TraceEvent traceEvent, CounterFilter filter, string sessionId, out ICounterPayload payload) + private static void HandleUpDownCounterValue(TraceEvent traceEvent, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)traceEvent.PayloadValue(0); - if (payloadSessionId != sessionId || traceEvent.Version < 1) // Version 1 added the value field. + if (payloadSessionId != configuration.SessionId || traceEvent.Version < 1) // Version 1 added the value field. { return; } @@ -219,7 +260,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe //string rateText = (string)traceEvent.PayloadValue(6); // Not currently using rate for UpDownCounters. string valueText = (string)traceEvent.PayloadValue(7); - if (!filter.IsIncluded(meterName, instrumentName)) + if (!configuration.CounterFilter.IsIncluded(meterName, instrumentName)) { return; } @@ -239,12 +280,13 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe } } - private static void HandleHistogram(TraceEvent obj, CounterFilter filter, string sessionId, out ICounterPayload payload) + private static void HandleHistogram(TraceEvent obj, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); - if (payloadSessionId != sessionId) + + if (payloadSessionId != configuration.SessionId) { return; } @@ -256,72 +298,71 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string tags = (string)obj.PayloadValue(5); string quantilesText = (string)obj.PayloadValue(6); - if (!filter.IsIncluded(meterName, instrumentName)) + if (!configuration.CounterFilter.IsIncluded(meterName, instrumentName)) { return; } //Note quantiles can be empty. IList quantiles = ParseQuantiles(quantilesText); - payload = new PercentilePayload(meterName, instrumentName, null, unit, tags, quantiles, obj.TimeStamp); - } - + payload = new AggregatePercentilePayload(meterName, instrumentName, null, unit, tags, quantiles, obj.TimeStamp); + } - private static void HandleHistogramLimitReached(TraceEvent obj, string sessionId, out ICounterPayload payload) + private static void HandleHistogramLimitReached(TraceEvent obj, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); - if (payloadSessionId != sessionId) + if (payloadSessionId != configuration.SessionId) { return; } - string errorMessage = $"Warning: Histogram tracking limit reached. Not all data is being shown. The limit can be changed with maxHistograms but will use more memory in the target process."; + string errorMessage = $"Warning: Histogram tracking limit ({configuration.MaxHistograms}) reached. Not all data is being shown. The limit can be changed but will use more memory in the target process."; - payload = new ErrorPayload(errorMessage); + payload = new ErrorPayload(errorMessage, obj.TimeStamp, EventType.HistogramLimitError); } - private static void HandleTimeSeriesLimitReached(TraceEvent obj, string sessionId, out ICounterPayload payload) + private static void HandleTimeSeriesLimitReached(TraceEvent obj, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); - if (payloadSessionId != sessionId) + if (payloadSessionId != configuration.SessionId) { return; } - string errorMessage = "Warning: Time series tracking limit reached. Not all data is being shown. The limit can be changed with maxTimeSeries but will use more memory in the target process."; + string errorMessage = $"Warning: Time series tracking limit ({configuration.MaxTimeseries}) reached. Not all data is being shown. The limit can be changed but will use more memory in the target process."; - payload = new ErrorPayload(errorMessage, obj.TimeStamp); + payload = new ErrorPayload(errorMessage, obj.TimeStamp, EventType.TimeSeriesLimitError); } - private static void HandleError(TraceEvent obj, string sessionId, out ICounterPayload payload) + private static void HandleError(TraceEvent obj, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); string error = (string)obj.PayloadValue(1); - if (payloadSessionId != sessionId) + if (configuration.SessionId != payloadSessionId) { return; } string errorMessage = "Error reported from target process:" + Environment.NewLine + error; - payload = new ErrorPayload(errorMessage, obj.TimeStamp); + payload = new ErrorPayload(errorMessage, obj.TimeStamp, EventType.ErrorTargetProcess); } - private static void HandleMultipleSessionsNotSupportedError(TraceEvent obj, string sessionId, out ICounterPayload payload) + private static void HandleMultipleSessionsNotSupportedError(TraceEvent obj, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); - if (payloadSessionId == sessionId) + if (payloadSessionId == configuration.SessionId) { // If our session is the one that is running then the error is not for us, // it is for some other session that came later @@ -332,7 +373,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string errorMessage = "Error: Another metrics collection session is already in progress for the target process." + Environment.NewLine + "Concurrent sessions are not supported."; - payload = new ErrorPayload(errorMessage, obj.TimeStamp); + payload = new ErrorPayload(errorMessage, obj.TimeStamp, EventType.MultipleSessionsNotSupportedError); } } @@ -383,20 +424,20 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe if (TryCreateSharedSessionConfiguredIncorrectlyMessage(obj, clientId, out string message)) { - payload = new ErrorPayload(message.ToString(), obj.TimeStamp); + payload = new ErrorPayload(message.ToString(), obj.TimeStamp, EventType.MultipleSessionsConfiguredIncorrectlyError); inactiveSharedSessions.Add(clientId); } } - private static void HandleObservableInstrumentCallbackError(TraceEvent obj, string sessionId, out ICounterPayload payload) + private static void HandleObservableInstrumentCallbackError(TraceEvent obj, CounterConfiguration configuration, out ICounterPayload payload) { payload = null; string payloadSessionId = (string)obj.PayloadValue(0); string error = (string)obj.PayloadValue(1); - if (payloadSessionId != sessionId) + if (payloadSessionId != configuration.SessionId) { return; } @@ -404,10 +445,10 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe string errorMessage = "Exception thrown from an observable instrument callback in the target process:" + Environment.NewLine + error; - payload = new ErrorPayload(errorMessage, obj.TimeStamp); + payload = new ErrorPayload(errorMessage, obj.TimeStamp, EventType.ObservableInstrumentCallbackError); } - private static IList ParseQuantiles(string quantileList) + private static List ParseQuantiles(string quantileList) { string[] quantileParts = quantileList.Split(';', StringSplitOptions.RemoveEmptyEntries); List quantiles = new(); @@ -418,11 +459,11 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { continue; } - if (!double.TryParse(keyValParts[0], out double key)) + if (!double.TryParse(keyValParts[0], NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double key)) { continue; } - if (!double.TryParse(keyValParts[1], out double val)) + if (!double.TryParse(keyValParts[1], NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double val)) { continue; } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/DiagnosticsEventPipeProcessor.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/DiagnosticsEventPipeProcessor.cs index 7ebc6409b..cff749c12 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/DiagnosticsEventPipeProcessor.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/DiagnosticsEventPipeProcessor.cs @@ -39,7 +39,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe _sessionStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } - public async Task Process(DiagnosticsClient client, TimeSpan duration, CancellationToken token) + public async Task Process(DiagnosticsClient client, TimeSpan duration, bool resumeRuntime, CancellationToken token) { //No need to guard against reentrancy here, since the calling pipeline does this already. IDisposable registration = token.Register(() => TryCancelCompletionSources(token)); @@ -53,7 +53,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe // Allows the event handling routines to stop processing before the duration expires. Func stopFunc = () => Task.Run(() => { streamProvider.StopProcessing(); }); - Stream sessionStream = await streamProvider.ProcessEvents(client, duration, token).ConfigureAwait(false); + Stream sessionStream = await streamProvider.ProcessEvents(client, duration, resumeRuntime, token).ConfigureAwait(false); if (!_sessionStarted.TrySetResult(true)) { diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventPipeStreamProvider.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventPipeStreamProvider.cs index 45cf97be4..23bb51b28 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventPipeStreamProvider.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventPipeStreamProvider.cs @@ -21,7 +21,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe _stopProcessingSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } - public async Task ProcessEvents(DiagnosticsClient client, TimeSpan duration, CancellationToken cancellationToken) + public async Task ProcessEvents(DiagnosticsClient client, TimeSpan duration, bool resumeRuntime, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -29,6 +29,17 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe try { session = await client.StartEventPipeSessionAsync(_sourceConfig.GetProviders(), _sourceConfig.RequestRundown, _sourceConfig.BufferSizeInMB, cancellationToken).ConfigureAwait(false); + if (resumeRuntime) + { + try + { + await client.ResumeRuntimeAsync(cancellationToken).ConfigureAwait(false); + } + catch (UnsupportedCommandException) + { + // Noop if the command is unknown since the target process is most likely a 3.1 app. + } + } } catch (EndOfStreamException e) { diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipeline.cs index 7da5c54ea..1b36f04e5 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipeline.cs @@ -35,7 +35,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { try { - return _processor.Value.Process(Client, Settings.Duration, token); + return _processor.Value.Process(Client, Settings.Duration, Settings.ResumeRuntime, token); } catch (InvalidOperationException e) { diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipelineSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipelineSettings.cs index ece20a236..1c35878eb 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipelineSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/EventSourcePipelineSettings.cs @@ -8,5 +8,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe internal class EventSourcePipelineSettings { public TimeSpan Duration { get; set; } + + public bool ResumeRuntime { get; set; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj index 0682848f1..c10400387 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj @@ -40,6 +40,7 @@ + diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Trace/EventTracePipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Trace/EventTracePipeline.cs index 68f176036..bb4d1fc3a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Trace/EventTracePipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Trace/EventTracePipeline.cs @@ -31,7 +31,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { //It is important that the underlying stream be completely read, or disposed. //If rundown is enabled, the underlying stream must be drained or disposed, or the app hangs. - using Stream eventStream = await _provider.Value.ProcessEvents(Client, Settings.Duration, token).ConfigureAwait(false); + using Stream eventStream = await _provider.Value.ProcessEvents(Client, Settings.Duration, Settings.ResumeRuntime, token).ConfigureAwait(false); await _onStreamAvailable(eventStream, token).ConfigureAwait(false); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs index 6350d4303..a1b96bf00 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs @@ -32,6 +32,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter private readonly CounterFilter _filter; private readonly EventCounterTriggerImpl _impl; private readonly string _providerName; + private CounterConfiguration _counterConfiguration; public EventCounterTrigger(EventCounterTriggerSettings settings) { @@ -45,6 +46,8 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter _filter = new CounterFilter(settings.CounterIntervalSeconds); _filter.AddFilter(settings.ProviderName, new string[] { settings.CounterName }); + _counterConfiguration = new CounterConfiguration(_filter); + _impl = new EventCounterTriggerImpl(settings); _providerName = settings.ProviderName; @@ -58,7 +61,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter public bool HasSatisfiedCondition(TraceEvent traceEvent) { // Filter to the counter of interest before forwarding to the implementation - if (traceEvent.TryGetCounterPayload(_filter, null, null, out ICounterPayload payload)) + if (traceEvent.TryGetCounterPayload(_counterConfiguration, out ICounterPayload payload)) { return _impl.HasSatisfiedCondition(payload); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs index 378e38906..d9e9a1267 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs @@ -29,6 +29,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsM private readonly string _meterName; private readonly string _clientId; private readonly string _sessionId; + private CounterConfiguration _counterConfiguration; public SystemDiagnosticsMetricsTrigger(SystemDiagnosticsMetricsTriggerSettings settings) { @@ -49,6 +50,10 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsM _clientId = settings.ClientId; _sessionId = settings.SessionId; + + _clientId = settings.ClientId; + + _counterConfiguration = new CounterConfiguration(_filter) { SessionId = _sessionId, ClientId = _clientId }; } public IReadOnlyDictionary> GetProviderEventMap() @@ -59,7 +64,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsM public bool HasSatisfiedCondition(TraceEvent traceEvent) { // Filter to the counter of interest before forwarding to the implementation - if (traceEvent.TryGetCounterPayload(_filter, _sessionId, _clientId, out ICounterPayload payload)) + if (traceEvent.TryGetCounterPayload(_counterConfiguration, out ICounterPayload payload)) { return _impl.HasSatisfiedCondition(payload); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs index cb60f1032..d0e4e2808 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs @@ -55,7 +55,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsM { EventType eventType = payload.EventType; - if (eventType == EventType.Error || eventType == EventType.CounterEnded) + if (eventType.IsError() || eventType == EventType.CounterEnded) { // not currently logging the error messages @@ -63,17 +63,17 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsM } else { - bool passesValueFilter = (payload is PercentilePayload percentilePayload) ? - _valueFilterHistogram(CreatePayloadDictionary(percentilePayload)) : + bool passesValueFilter = (payload is AggregatePercentilePayload aggregatePercentilePayload) ? + _valueFilterHistogram(CreatePayloadDictionary(aggregatePercentilePayload)) : _valueFilterDefault(payload.Value); return SharedTriggerImplHelper.HasSatisfiedCondition(ref _latestTicks, ref _targetTicks, _windowTicks, _intervalTicks, payload, passesValueFilter); } } - private static Dictionary CreatePayloadDictionary(PercentilePayload percentilePayload) + private static Dictionary CreatePayloadDictionary(AggregatePercentilePayload aggregatePercentilePayload) { - return percentilePayload.Quantiles.ToDictionary(keySelector: p => CounterUtilities.CreatePercentile(p.Percentage), elementSelector: p => p.Value); + return aggregatePercentilePayload.Quantiles.ToDictionary(keySelector: q => CounterUtilities.CreatePercentile(q.Percentage), elementSelector: q => q.Value); } } } diff --git a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj index 975476fa6..a4fff0796 100644 --- a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj +++ b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj @@ -26,6 +26,8 @@ + + diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index a2a623d0a..04159229a 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -6,45 +6,31 @@ using System.Collections.Generic; using System.CommandLine; using System.CommandLine.IO; using System.CommandLine.Rendering; +using System.ComponentModel; using System.Diagnostics; -using System.Diagnostics.Tracing; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Diagnostics.Monitoring; using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.Tools.Counters.Exporters; -using Microsoft.Diagnostics.Tracing; using Microsoft.Internal.Common.Utils; namespace Microsoft.Diagnostics.Tools.Counters { - public class CounterMonitor + internal class CounterMonitor : ICountersLogger { private const int BufferDelaySecs = 1; - private const string SharedSessionId = "SHARED"; // This should be identical to the one used by dotnet-monitor in MetricSourceConfiguration.cs - private static HashSet inactiveSharedSessions = new(StringComparer.OrdinalIgnoreCase); - - private string _sessionId; private int _processId; - private int _interval; private CounterSet _counterList; - private CancellationToken _ct; private IConsole _console; private ICounterRenderer _renderer; private string _output; private bool _pauseCmdSet; - private readonly TaskCompletionSource _shouldExit; - private bool _resumeRuntime; + private readonly TaskCompletionSource _shouldExit; private DiagnosticsClient _diagnosticsClient; - private EventPipeSession _session; - private readonly string _clientId; - private int _maxTimeSeries; - private int _maxHistograms; - private TimeSpan _duration; + private MetricsPipelineSettings _settings; private class ProviderEventState { @@ -57,77 +43,7 @@ namespace Microsoft.Diagnostics.Tools.Counters public CounterMonitor() { _pauseCmdSet = false; - _clientId = Guid.NewGuid().ToString(); - - _shouldExit = new TaskCompletionSource(); - } - - private void DynamicAllMonitor(TraceEvent obj) - { - if (_shouldExit.Task.IsCompleted) - { - return; - } - - lock (this) - { - // If we are paused, ignore the event. - // There's a potential race here between the two tasks but not a huge deal if we miss by one event. - _renderer.ToggleStatus(_pauseCmdSet); - - // If a session received a MultipleSessionsConfiguredIncorrectlyError, ignore future shared events - if (obj.ProviderName == "System.Diagnostics.Metrics" && !inactiveSharedSessions.Contains(_clientId)) - { - if (obj.EventName == "BeginInstrumentReporting") - { - HandleBeginInstrumentReporting(obj); - } - if (obj.EventName == "HistogramValuePublished") - { - HandleHistogram(obj); - } - else if (obj.EventName == "GaugeValuePublished") - { - HandleGauge(obj); - } - else if (obj.EventName == "CounterRateValuePublished") - { - HandleCounterRate(obj); - } - else if (obj.EventName == "UpDownCounterRateValuePublished") - { - HandleUpDownCounterValue(obj); - } - else if (obj.EventName == "TimeSeriesLimitReached") - { - HandleTimeSeriesLimitReached(obj); - } - else if (obj.EventName == "HistogramLimitReached") - { - HandleHistogramLimitReached(obj); - } - else if (obj.EventName == "Error") - { - HandleError(obj); - } - else if (obj.EventName == "ObservableInstrumentCallbackError") - { - HandleObservableInstrumentCallbackError(obj); - } - else if (obj.EventName == "MultipleSessionsNotSupportedError") - { - HandleMultipleSessionsNotSupportedError(obj); - } - else if (obj.EventName == "MultipleSessionsConfiguredIncorrectlyError") - { - HandleMultipleSessionsConfiguredIncorrectlyError(obj); - } - } - else if (obj.EventName == "EventCounters") - { - HandleDiagnosticCounter(obj); - } - } + _shouldExit = new TaskCompletionSource(); } private void MeterInstrumentEventObserved(string meterName, DateTime timestamp) @@ -147,255 +63,16 @@ namespace Microsoft.Diagnostics.Tools.Counters } } - private void HandleBeginInstrumentReporting(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - string meterName = (string)obj.PayloadValue(1); - // string instrumentName = (string)obj.PayloadValue(3); - if (sessionId != _sessionId) - { - return; - } - MeterInstrumentEventObserved(meterName, obj.TimeStamp); - } - - private void HandleCounterRate(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - string meterName = (string)obj.PayloadValue(1); - //string meterVersion = (string)obj.PayloadValue(2); - string instrumentName = (string)obj.PayloadValue(3); - string unit = (string)obj.PayloadValue(4); - string tags = (string)obj.PayloadValue(5); - string rateText = (string)obj.PayloadValue(6); - if (sessionId != _sessionId || !Filter(meterName, instrumentName)) - { - return; - } - MeterInstrumentEventObserved(meterName, obj.TimeStamp); - - // the value might be an empty string indicating no measurement was provided this collection interval - if (double.TryParse(rateText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double rate)) - { - CounterPayload payload = new RatePayload(meterName, instrumentName, null, unit, tags, rate, _interval, obj.TimeStamp); - _renderer.CounterPayloadReceived(payload, _pauseCmdSet); - } - - } - - private void HandleGauge(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - string meterName = (string)obj.PayloadValue(1); - //string meterVersion = (string)obj.PayloadValue(2); - string instrumentName = (string)obj.PayloadValue(3); - string unit = (string)obj.PayloadValue(4); - string tags = (string)obj.PayloadValue(5); - string lastValueText = (string)obj.PayloadValue(6); - if (sessionId != _sessionId || !Filter(meterName, instrumentName)) - { - return; - } - MeterInstrumentEventObserved(meterName, obj.TimeStamp); - - // the value might be an empty string indicating no measurement was provided this collection interval - if (double.TryParse(lastValueText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double lastValue)) - { - CounterPayload payload = new GaugePayload(meterName, instrumentName, null, unit, tags, lastValue, obj.TimeStamp); - _renderer.CounterPayloadReceived(payload, _pauseCmdSet); - } - else - { - // for observable instruments we assume the lack of data is meaningful and remove it from the UI - CounterPayload payload = new RatePayload(meterName, instrumentName, null, unit, tags, 0, _interval, obj.TimeStamp); - _renderer.CounterStopped(payload); - } - } - - private void HandleUpDownCounterValue(TraceEvent obj) - { - if (obj.Version < 1) // Version 1 added the value field. - { - return; - } - - string sessionId = (string)obj.PayloadValue(0); - string meterName = (string)obj.PayloadValue(1); - //string meterVersion = (string)obj.PayloadValue(2); - string instrumentName = (string)obj.PayloadValue(3); - string unit = (string)obj.PayloadValue(4); - string tags = (string)obj.PayloadValue(5); - //string rateText = (string)obj.PayloadValue(6); // Not currently using rate for UpDownCounters. - string valueText = (string)obj.PayloadValue(7); - if (sessionId != _sessionId || !Filter(meterName, instrumentName)) - { - return; - } - MeterInstrumentEventObserved(meterName, obj.TimeStamp); - - // the value might be an empty string indicating no measurement was provided this collection interval - if (double.TryParse(valueText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) - { - // UpDownCounter reports the value, not the rate - this is different than how Counter behaves, and is thus treated as a gauge. - CounterPayload payload = new GaugePayload(meterName, instrumentName, null, unit, tags, value, obj.TimeStamp); - _renderer.CounterPayloadReceived(payload, _pauseCmdSet); - } - else - { - // for observable instruments we assume the lack of data is meaningful and remove it from the UI - CounterPayload payload = new RatePayload(meterName, instrumentName, null, unit, tags, 0, _interval, obj.TimeStamp); - _renderer.CounterStopped(payload); - } - } - - private void HandleHistogram(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - string meterName = (string)obj.PayloadValue(1); - //string meterVersion = (string)obj.PayloadValue(2); - string instrumentName = (string)obj.PayloadValue(3); - string unit = (string)obj.PayloadValue(4); - string tags = (string)obj.PayloadValue(5); - string quantilesText = (string)obj.PayloadValue(6); - if (sessionId != _sessionId || !Filter(meterName, instrumentName)) - { - return; - } - MeterInstrumentEventObserved(meterName, obj.TimeStamp); - KeyValuePair[] quantiles = ParseQuantiles(quantilesText); - foreach ((double key, double val) in quantiles) - { - CounterPayload payload = new PercentilePayload(meterName, instrumentName, null, unit, AppendQuantile(tags, $"Percentile={key * 100}"), val, obj.TimeStamp); - _renderer.CounterPayloadReceived(payload, _pauseCmdSet); - } - } - - private void HandleHistogramLimitReached(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - if (sessionId != _clientId) - { - return; - } - _renderer.SetErrorText( - $"Warning: Histogram tracking limit ({_maxHistograms}) reached. Not all data is being shown." + Environment.NewLine + - "The limit can be changed with --maxHistograms but will use more memory in the target process." - ); - } - - private void HandleTimeSeriesLimitReached(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - if (sessionId != _sessionId) - { - return; - } - _renderer.SetErrorText( - $"Warning: Time series tracking limit ({_maxTimeSeries}) reached. Not all data is being shown." + Environment.NewLine + - "The limit can be changed with --maxTimeSeries but will use more memory in the target process." - ); - } - - private void HandleError(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - string error = (string)obj.PayloadValue(1); - if (sessionId != _sessionId) - { - return; - } - _renderer.SetErrorText( - "Error reported from target process:" + Environment.NewLine + - error - ); - _shouldExit.TrySetResult((int)ReturnCode.TracingError); - } - - private void HandleObservableInstrumentCallbackError(TraceEvent obj) - { - string sessionId = (string)obj.PayloadValue(0); - string error = (string)obj.PayloadValue(1); - if (sessionId != _sessionId) - { - return; - } - _renderer.SetErrorText( - "Exception thrown from an observable instrument callback in the target process:" + Environment.NewLine + - error - ); - } - - private void HandleMultipleSessionsNotSupportedError(TraceEvent obj) - { - string runningSessionId = (string)obj.PayloadValue(0); - if (runningSessionId == _sessionId) - { - // If our session is the one that is running then the error is not for us, - // it is for some other session that came later - return; - } - _renderer.SetErrorText( - "Error: Another metrics collection session is already in progress for the target process." + Environment.NewLine + - "Concurrent sessions are not supported."); - _shouldExit.TrySetResult((int)ReturnCode.SessionCreationError); - } - - private void HandleMultipleSessionsConfiguredIncorrectlyError(TraceEvent obj) - { - if (TraceEventExtensions.TryCreateSharedSessionConfiguredIncorrectlyMessage(obj, _clientId, out string message)) - { - _renderer.SetErrorText(message); - inactiveSharedSessions.Add(_clientId); - _shouldExit.TrySetResult((int)ReturnCode.SessionCreationError); - } - } - - private static KeyValuePair[] ParseQuantiles(string quantileList) - { - string[] quantileParts = quantileList.Split(';', StringSplitOptions.RemoveEmptyEntries); - List> quantiles = new(); - foreach (string quantile in quantileParts) - { - string[] keyValParts = quantile.Split('=', StringSplitOptions.RemoveEmptyEntries); - if (keyValParts.Length != 2) - { - continue; - } - if (!double.TryParse(keyValParts[0], NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double key)) - { - continue; - } - if (!double.TryParse(keyValParts[1], NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double val)) - { - continue; - } - quantiles.Add(new KeyValuePair(key, val)); - } - return quantiles.ToArray(); - } - - private static string AppendQuantile(string tags, string quantile) => string.IsNullOrEmpty(tags) ? quantile : $"{tags},{quantile}"; - - private void HandleDiagnosticCounter(TraceEvent obj) + private void HandleDiagnosticCounter(ICounterPayload payload) { - IDictionary payloadVal = (IDictionary)(obj.PayloadValue(0)); - IDictionary payloadFields = (IDictionary)(payloadVal["Payload"]); - - // If it's not a counter we asked for, ignore it. - string name = payloadFields["Name"].ToString(); - if (!_counterList.Contains(obj.ProviderName, name)) - { - return; - } - // init providerEventState if this is the first time we've seen an event from this provider - if (!_providerEventStates.TryGetValue(obj.ProviderName, out ProviderEventState providerState)) + if (!_providerEventStates.TryGetValue(payload.Provider, out ProviderEventState providerState)) { providerState = new ProviderEventState() { - FirstReceiveTimestamp = obj.TimeStamp + FirstReceiveTimestamp = payload.Timestamp }; - _providerEventStates.Add(obj.ProviderName, providerState); + _providerEventStates.Add(payload.Provider, providerState); } // we give precedence to instrument events over diagnostic counter events. If we are seeing @@ -405,42 +82,35 @@ namespace Microsoft.Diagnostics.Tools.Counters return; } - CounterPayload payload; - if (payloadFields["CounterType"].Equals("Sum")) - { - payload = new RatePayload( - obj.ProviderName, - name, - payloadFields["DisplayName"].ToString(), - payloadFields["DisplayUnits"].ToString(), - null, - (double)payloadFields["Increment"], - _interval, - obj.TimeStamp); - } - else - { - payload = new GaugePayload( - obj.ProviderName, - name, - payloadFields["DisplayName"].ToString(), - payloadFields["DisplayUnits"].ToString(), - null, - (double)payloadFields["Mean"], - obj.TimeStamp); - } - // If we saw the first event for this provider recently then a duplicate instrument event may still be // coming. We'll buffer this event for a while and then render it if it remains unduplicated for // a while. // This is all best effort, if we do show the DiagnosticCounter event and then an instrument event shows up - // later the renderer may obsserve some odd behavior like changes in the counter metadata, oddly timed reporting + // later the renderer may observe some odd behavior like changes in the counter metadata, oddly timed reporting // intervals, or counters that stop reporting. // I'm gambling this is good enough that the behavior will never be seen in practice, but if it is we could // either adjust the time delay or try to improve how the renderers handle it. - if (providerState.FirstReceiveTimestamp + TimeSpan.FromSeconds(BufferDelaySecs) >= obj.TimeStamp) + if (providerState.FirstReceiveTimestamp + TimeSpan.FromSeconds(BufferDelaySecs) >= payload.Timestamp) + { + _bufferedEvents.Enqueue((CounterPayload)payload); + } + else { - _bufferedEvents.Enqueue(payload); + CounterPayloadReceived((CounterPayload)payload); + } + } + + private void CounterPayloadReceived(CounterPayload payload) + { + if (payload is AggregatePercentilePayload aggregatePayload) + { + foreach (Quantile quantile in aggregatePayload.Quantiles) + { + (double key, double val) = quantile; + PercentilePayload percentilePayload = new(payload.Provider, payload.Name, payload.DisplayName, payload.Unit, AppendQuantile(payload.Metadata, $"Percentile={key * 100}"), val, payload.Timestamp); + _renderer.CounterPayloadReceived(percentilePayload, _pauseCmdSet); + } + } else { @@ -448,6 +118,8 @@ namespace Microsoft.Diagnostics.Tools.Counters } } + private static string AppendQuantile(string tags, string quantile) => string.IsNullOrEmpty(tags) ? quantile : $"{tags},{quantile}"; + // when receiving DiagnosticCounter events we may have buffered them to wait for // duplicate instrument events. If we've waited long enough then we should remove // them from the buffer and render them. @@ -459,7 +131,7 @@ namespace Microsoft.Diagnostics.Tools.Counters while (_bufferedEvents.Count != 0) { CounterPayload payload = _bufferedEvents.Peek(); - ProviderEventState providerEventState = _providerEventStates[payload.ProviderName]; + ProviderEventState providerEventState = _providerEventStates[payload.Provider]; if (providerEventState.InstrumentEventObserved) { _bufferedEvents.Dequeue(); @@ -467,7 +139,7 @@ namespace Microsoft.Diagnostics.Tools.Counters else if (providerEventState.FirstReceiveTimestamp + TimeSpan.FromSeconds(BufferDelaySecs) < now) { _bufferedEvents.Dequeue(); - _renderer.CounterPayloadReceived(payload, _pauseCmdSet); + CounterPayloadReceived((CounterPayload)payload); } else { @@ -481,37 +153,7 @@ namespace Microsoft.Diagnostics.Tools.Counters } } - private void StopMonitor() - { - try - { - _session?.Stop(); - } - catch (EndOfStreamException ex) - { - // If the app we're monitoring exits abruptly, this may throw in which case we just swallow the exception and exit gracefully. - Debug.WriteLine($"[ERROR] {ex}"); - } - // We may time out if the process ended before we sent StopTracing command. We can just exit in that case. - catch (TimeoutException) - { - } - // On Unix platforms, we may actually get a PNSE since the pipe is gone with the process, and Runtime Client Library - // does not know how to distinguish a situation where there is no pipe to begin with, or where the process has exited - // before dotnet-counters and got rid of a pipe that once existed. - // Since we are catching this in StopMonitor() we know that the pipe once existed (otherwise the exception would've - // been thrown in StartMonitor directly) - catch (PlatformNotSupportedException) - { - } - // On non-abrupt exits, the socket may be already closed by the runtime and we won't be able to send a stop request through it. - catch (ServerNotAvailableException) - { - } - _renderer.Stop(); - } - - public async Task Monitor( + public async Task Monitor( CancellationToken ct, List counter_list, string counters, @@ -535,7 +177,7 @@ namespace Microsoft.Diagnostics.Tools.Counters ValidateNonNegative(maxTimeSeries, nameof(maxTimeSeries)); if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out _processId)) { - return (int)ReturnCode.ArgumentError; + return ReturnCode.ArgumentError; } ct.Register(() => _shouldExit.TrySetResult((int)ReturnCode.Ok)); @@ -546,7 +188,7 @@ namespace Microsoft.Diagnostics.Tools.Counters bool useAnsi = vTerm.IsEnabled; if (holder == null) { - return (int)ReturnCode.Ok; + return ReturnCode.Ok; } try { @@ -554,38 +196,48 @@ namespace Microsoft.Diagnostics.Tools.Counters // the launch command may misinterpret app arguments as the old space separated // provider list so we need to ignore it in that case _counterList = ConfigureCounters(counters, _processId != 0 ? counter_list : null); - _ct = ct; - _interval = refreshInterval; - _maxHistograms = maxHistograms; - _maxTimeSeries = maxTimeSeries; _renderer = new ConsoleWriter(useAnsi); _diagnosticsClient = holder.Client; - _resumeRuntime = resumeRuntime; - _duration = duration; - int ret = await Start().ConfigureAwait(false); + _settings = new MetricsPipelineSettings(); + _settings.Duration = duration == TimeSpan.Zero ? Timeout.InfiniteTimeSpan : duration; + _settings.MaxHistograms = maxHistograms; + _settings.MaxTimeSeries = maxTimeSeries; + _settings.CounterIntervalSeconds = refreshInterval; + _settings.ResumeRuntime = resumeRuntime; + _settings.CounterGroups = GetEventPipeProviders(); + + bool useSharedSession = false; + if (_diagnosticsClient.GetProcessInfo().TryGetProcessClrVersion(out Version version)) + { + useSharedSession = version.Major >= 8 ? true : false; + } + _settings.UseSharedSession = useSharedSession; + + ReturnCode ret; + MetricsPipeline eventCounterPipeline = new(holder.Client, _settings, new[] { this }); + await using (eventCounterPipeline.ConfigureAwait(false)) + { + ret = await Start(eventCounterPipeline, ct).ConfigureAwait(false); + } ProcessLauncher.Launcher.Cleanup(); return ret; } catch (OperationCanceledException) { - try - { - _session.Stop(); - } - catch (Exception) { } // Swallow all exceptions for now. + //Cancellation token should automatically stop the session console.Out.WriteLine($"Complete"); - return (int)ReturnCode.Ok; + return ReturnCode.Ok; } } } catch (CommandLineErrorException e) { console.Error.WriteLine(e.Message); - return (int)ReturnCode.ArgumentError; + return ReturnCode.ArgumentError; } } - public async Task Collect( + public async Task Collect( CancellationToken ct, List counter_list, string counters, @@ -611,7 +263,7 @@ namespace Microsoft.Diagnostics.Tools.Counters ValidateNonNegative(maxTimeSeries, nameof(maxTimeSeries)); if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out _processId)) { - return (int)ReturnCode.ArgumentError; + return ReturnCode.ArgumentError; } ct.Register(() => _shouldExit.TrySetResult((int)ReturnCode.Ok)); @@ -629,17 +281,19 @@ namespace Microsoft.Diagnostics.Tools.Counters // the launch command may misinterpret app arguments as the old space separated // provider list so we need to ignore it in that case _counterList = ConfigureCounters(counters, _processId != 0 ? counter_list : null); - _ct = ct; - _interval = refreshInterval; - _maxHistograms = maxHistograms; - _maxTimeSeries = maxTimeSeries; + _settings = new MetricsPipelineSettings(); + _settings.Duration = duration == TimeSpan.Zero ? Timeout.InfiniteTimeSpan : duration; + _settings.MaxHistograms = maxHistograms; + _settings.MaxTimeSeries = maxTimeSeries; + _settings.CounterIntervalSeconds = refreshInterval; + _settings.ResumeRuntime = resumeRuntime; + _settings.CounterGroups = GetEventPipeProviders(); _output = output; _diagnosticsClient = holder.Client; - _duration = duration; if (_output.Length == 0) { _console.Error.WriteLine("Output cannot be an empty string"); - return (int)ReturnCode.ArgumentError; + return ReturnCode.ArgumentError; } if (format == CountersExportFormat.csv) { @@ -663,27 +317,29 @@ namespace Microsoft.Diagnostics.Tools.Counters else { _console.Error.WriteLine($"The output format {format} is not a valid output format."); - return (int)ReturnCode.ArgumentError; + return ReturnCode.ArgumentError; } - _resumeRuntime = resumeRuntime; - int ret = await Start().ConfigureAwait(false); + + ReturnCode ret; + MetricsPipeline eventCounterPipeline = new(holder.Client, _settings, new[] { this }); + await using (eventCounterPipeline.ConfigureAwait(false)) + { + ret = await Start(pipeline: eventCounterPipeline, ct).ConfigureAwait(false); + } + return ret; } catch (OperationCanceledException) { - try - { - _session.Stop(); - } - catch (Exception) { } // session.Stop() can throw if target application already stopped before we send the stop command. - return (int)ReturnCode.Ok; + //Cancellation token should automatically stop the session + return ReturnCode.Ok; } } } catch (CommandLineErrorException e) { console.Error.WriteLine(e.Message); - return (int)ReturnCode.ArgumentError; + return ReturnCode.ArgumentError; } } @@ -833,85 +489,21 @@ namespace Microsoft.Diagnostics.Tools.Counters } } - private EventPipeProvider[] GetEventPipeProviders() - { - // EventSources support EventCounter based metrics directly - IEnumerable eventCounterProviders = _counterList.Providers.Select( - providerName => new EventPipeProvider(providerName, EventLevel.Error, 0, new Dictionary() - {{ "EventCounterIntervalSec", _interval.ToString() }})); - - //System.Diagnostics.Metrics EventSource supports the new Meter/Instrument APIs - const long TimeSeriesValues = 0x2; - StringBuilder metrics = new(); - foreach (string provider in _counterList.Providers) + private EventPipeCounterGroup[] GetEventPipeProviders() => + _counterList.Providers.Select(provider => new EventPipeCounterGroup { - if (metrics.Length != 0) - { - metrics.Append(','); - } - if (_counterList.IncludesAllCounters(provider)) - { - metrics.Append(provider); - } - else - { - string[] providerCounters = _counterList.GetCounters(provider).Select(counter => $"{provider}\\{counter}").ToArray(); - metrics.Append(string.Join(',', providerCounters)); - } - } + ProviderName = provider, + CounterNames = _counterList.GetCounters(provider).ToArray() + }).ToArray(); - // Shared Session Id was added in 8.0 - older runtimes will not properly support it. - _sessionId = Guid.NewGuid().ToString(); - if (_diagnosticsClient.GetProcessInfo().TryGetProcessClrVersion(out Version version)) - { - _sessionId = version.Major >= 8 ? SharedSessionId : _sessionId; - } - - EventPipeProvider metricsEventSourceProvider = - new("System.Diagnostics.Metrics", EventLevel.Informational, TimeSeriesValues, - new Dictionary() - { - { "SessionId", _sessionId }, - { "Metrics", metrics.ToString() }, - { "RefreshInterval", _interval.ToString() }, - { "MaxTimeSeries", _maxTimeSeries.ToString() }, - { "MaxHistograms", _maxHistograms.ToString() }, - { "ClientId", _clientId } - } - ); - - return eventCounterProviders.Append(metricsEventSourceProvider).ToArray(); - } - - private bool Filter(string meterName, string instrumentName) - { - return _counterList.GetCounters(meterName).Contains(instrumentName) || _counterList.IncludesAllCounters(meterName); - } - - private Task Start() + private async Task Start(MetricsPipeline pipeline, CancellationToken token) { - EventPipeProvider[] providers = GetEventPipeProviders(); _renderer.Initialize(); - - Task monitorTask = new(() => { + Task monitorTask = new(async () => { try { - _session = _diagnosticsClient.StartEventPipeSession(providers, false, 10); - if (_resumeRuntime) - { - try - { - _diagnosticsClient.ResumeRuntime(); - } - catch (UnsupportedCommandException) - { - // Noop if the command is unknown since the target process is most likely a 3.1 app. - } - } - EventPipeEventSource source = new(_session.EventStream); - source.Dynamic.All += DynamicAllMonitor; - _renderer.EventPipeSourceConnected(); - source.Process(); + Task runAsyncTask = await pipeline.StartAsync(token).ConfigureAwait(false); + await runAsyncTask.ConfigureAwait(false); } catch (DiagnosticsClientException ex) { @@ -928,15 +520,8 @@ namespace Microsoft.Diagnostics.Tools.Counters }); monitorTask.Start(); - bool shouldStopAfterDuration = _duration != default(TimeSpan); - Stopwatch durationStopwatch = null; - if (shouldStopAfterDuration) - { - durationStopwatch = Stopwatch.StartNew(); - } - - while (!_shouldExit.Task.Wait(250)) + while (!_shouldExit.Task.Wait(250, token)) { HandleBufferedEvents(); if (!Console.IsInputRedirected && Console.KeyAvailable) @@ -955,16 +540,107 @@ namespace Microsoft.Diagnostics.Tools.Counters _pauseCmdSet = false; } } + } + + try + { + await pipeline.StopAsync(token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (PipelineException) + { + } + + return await _shouldExit.Task.ConfigureAwait(false); + } + + void ICountersLogger.Log(ICounterPayload payload) + { + if (_shouldExit.Task.IsCompleted) + { + return; + } - if (shouldStopAfterDuration && durationStopwatch.Elapsed >= _duration) + lock (this) + { + // If we are paused, ignore the event. + // There's a potential race here between the two tasks but not a huge deal if we miss by one event. + _renderer.ToggleStatus(_pauseCmdSet); + if (payload is ErrorPayload errorPayload) { - durationStopwatch.Stop(); - break; + // Several of the error messages used by Dotnet are specific to the tool; + // the error messages found in errorPayload.ErrorMessage are not tool-specific. + // This replaces the generic error messages with specific ones as-needed. + string errorMessage = string.Empty; + switch (errorPayload.EventType) + { + case EventType.HistogramLimitError: + errorMessage = $"Warning: Histogram tracking limit ({_settings.MaxHistograms}) reached. Not all data is being shown." + Environment.NewLine + + "The limit can be changed with --maxHistograms but will use more memory in the target process."; + break; + case EventType.TimeSeriesLimitError: + errorMessage = $"Warning: Time series tracking limit ({_settings.MaxTimeSeries}) reached. Not all data is being shown." + Environment.NewLine + + "The limit can be changed with --maxTimeSeries but will use more memory in the target process."; + break; + case EventType.ErrorTargetProcess: + case EventType.MultipleSessionsNotSupportedError: + case EventType.MultipleSessionsConfiguredIncorrectlyError: + case EventType.ObservableInstrumentCallbackError: + default: + errorMessage = errorPayload.ErrorMessage; + break; + } + + _renderer.SetErrorText(errorMessage); + + if (errorPayload.EventType.IsSessionStartupError()) + { + _shouldExit.TrySetResult(ReturnCode.SessionCreationError); + } + else if (errorPayload.EventType.IsTracingError()) + { + _shouldExit.TrySetResult(ReturnCode.TracingError); + } + else if (errorPayload.EventType.IsNonFatalError()) + { + // Don't need to exit for NonFatalError + } + else + { + _shouldExit.TrySetResult(ReturnCode.UnknownError); + } + } + else if (payload is CounterEndedPayload counterEnded) + { + _renderer.CounterStopped(counterEnded); + } + else if (payload.IsMeter) + { + MeterInstrumentEventObserved(payload.Provider, payload.Timestamp); + if (payload.EventType.IsValuePublishedEvent()) + { + CounterPayloadReceived((CounterPayload)payload); + } + } + else + { + HandleDiagnosticCounter(payload); } } + } + + public Task PipelineStarted(CancellationToken token) + { + _renderer.EventPipeSourceConnected(); + return Task.CompletedTask; + } - StopMonitor(); - return _shouldExit.Task; + public Task PipelineStopped(CancellationToken token) + { + _renderer.Stop(); + return Task.CompletedTask; } } } diff --git a/src/Tools/dotnet-counters/CounterPayload.cs b/src/Tools/dotnet-counters/CounterPayload.cs deleted file mode 100644 index a7863bd14..000000000 --- a/src/Tools/dotnet-counters/CounterPayload.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.Diagnostics.Tools.Counters -{ - public class CounterPayload - { - public CounterPayload(string providerName, string name, string displayName, string displayUnits, string tags, double value, DateTime timestamp, string type) - { - ProviderName = providerName; - Name = name; - Tags = tags; - Value = value; - Timestamp = timestamp; - CounterType = type; - } - - public string ProviderName { get; private set; } - public string Name { get; private set; } - public double Value { get; private set; } - public virtual string DisplayName { get; protected set; } - public string CounterType { get; private set; } - public DateTime Timestamp { get; private set; } - public string Tags { get; private set; } - } - - internal class GaugePayload : CounterPayload - { - public GaugePayload(string providerName, string name, string displayName, string displayUnits, string tags, double value, DateTime timestamp) : - base(providerName, name, displayName, displayUnits, tags, value, timestamp, "Metric") - { - // In case these properties are not provided, set them to appropriate values. - string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; - DisplayName = !string.IsNullOrEmpty(displayUnits) ? $"{counterName} ({displayUnits})" : counterName; - } - } - - internal class RatePayload : CounterPayload - { - public RatePayload(string providerName, string name, string displayName, string displayUnits, string tags, double value, double intervalSecs, DateTime timestamp) : - base(providerName, name, displayName, displayUnits, tags, value, timestamp, "Rate") - { - // In case these properties are not provided, set them to appropriate values. - string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; - string unitsName = string.IsNullOrEmpty(displayUnits) ? "Count" : displayUnits; - string intervalName = intervalSecs.ToString() + " sec"; - DisplayName = $"{counterName} ({unitsName} / {intervalName})"; - } - } - - internal class PercentilePayload : CounterPayload - { - public PercentilePayload(string providerName, string name, string displayName, string displayUnits, string tags, double val, DateTime timestamp) : - base(providerName, name, displayName, displayUnits, tags, val, timestamp, "Metric") - { - // In case these properties are not provided, set them to appropriate values. - string counterName = string.IsNullOrEmpty(displayName) ? name : displayName; - DisplayName = !string.IsNullOrEmpty(displayUnits) ? $"{counterName} ({displayUnits})" : counterName; - } - } -} diff --git a/src/Tools/dotnet-counters/CounterPayloadExtensions.cs b/src/Tools/dotnet-counters/CounterPayloadExtensions.cs new file mode 100644 index 000000000..974e054c9 --- /dev/null +++ b/src/Tools/dotnet-counters/CounterPayloadExtensions.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.Monitoring.EventPipe; + +namespace Microsoft.Diagnostics.Tools.Counters +{ + internal static class CounterPayloadExtensions + { + public static string GetDisplay(this ICounterPayload counterPayload) + { + if (!counterPayload.IsMeter) + { + string unit = counterPayload.Unit == "count" ? "Count" : counterPayload.Unit; + if (counterPayload.CounterType == CounterType.Rate) + { + return $"{counterPayload.DisplayName} ({unit} / {counterPayload.Series} sec)"; + } + if (!string.IsNullOrEmpty(counterPayload.Unit)) + { + return $"{counterPayload.DisplayName} ({unit})"; + } + } + + return $"{counterPayload.DisplayName}"; + } + } +} diff --git a/src/Tools/dotnet-counters/Exporters/CSVExporter.cs b/src/Tools/dotnet-counters/Exporters/CSVExporter.cs index cdf760edb..eb4399de9 100644 --- a/src/Tools/dotnet-counters/Exporters/CSVExporter.cs +++ b/src/Tools/dotnet-counters/Exporters/CSVExporter.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.IO; using System.Text; +using Microsoft.Diagnostics.Monitoring.EventPipe; namespace Microsoft.Diagnostics.Tools.Counters.Exporters { @@ -70,11 +71,11 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters builder .Append(payload.Timestamp.ToString()).Append(',') - .Append(payload.ProviderName).Append(',') - .Append(payload.DisplayName); - if (!string.IsNullOrEmpty(payload.Tags)) + .Append(payload.Provider).Append(',') + .Append(payload.GetDisplay()); + if (!string.IsNullOrEmpty(payload.Metadata)) { - builder.Append('[').Append(payload.Tags.Replace(',', ';')).Append(']'); + builder.Append('[').Append(payload.Metadata.Replace(',', ';')).Append(']'); } builder.Append(',') .Append(payload.CounterType).Append(',') diff --git a/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs b/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs index 76f06931f..8795ba4e7 100644 --- a/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs +++ b/src/Tools/dotnet-counters/Exporters/ConsoleWriter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.Diagnostics.Monitoring.EventPipe; namespace Microsoft.Diagnostics.Tools.Counters.Exporters { @@ -12,7 +13,7 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters /// ConsoleWriter is an implementation of ICounterRenderer for rendering the counter values in real-time /// to the console. This is the renderer for the `dotnet-counters monitor` command. /// - public class ConsoleWriter : ICounterRenderer + internal class ConsoleWriter : ICounterRenderer { /// Information about an observed provider. private class ObservedProvider @@ -257,9 +258,9 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters return; } - string providerName = payload.ProviderName; + string providerName = payload.Provider; string name = payload.Name; - string tags = payload.Tags; + string tags = payload.Metadata; bool redraw = false; if (!_providers.TryGetValue(providerName, out ObservedProvider provider)) @@ -270,7 +271,7 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters if (!provider.Counters.TryGetValue(name, out ObservedCounter counter)) { - string displayName = payload.DisplayName; + string displayName = payload.GetDisplay(); provider.Counters[name] = counter = new ObservedCounter(displayName); _maxNameLength = Math.Max(_maxNameLength, displayName.Length); if (tags != null) @@ -313,9 +314,9 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters { lock (_lock) { - string providerName = payload.ProviderName; + string providerName = payload.Provider; string counterName = payload.Name; - string tags = payload.Tags; + string tags = payload.Metadata; if (!_providers.TryGetValue(providerName, out ObservedProvider provider)) { diff --git a/src/Tools/dotnet-counters/Exporters/ICounterRenderer.cs b/src/Tools/dotnet-counters/Exporters/ICounterRenderer.cs index d8389b2ec..2bd4ca254 100644 --- a/src/Tools/dotnet-counters/Exporters/ICounterRenderer.cs +++ b/src/Tools/dotnet-counters/Exporters/ICounterRenderer.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Diagnostics.Monitoring.EventPipe; + namespace Microsoft.Diagnostics.Tools.Counters.Exporters { - public interface ICounterRenderer + internal interface ICounterRenderer { void Initialize(); void EventPipeSourceConnected(); diff --git a/src/Tools/dotnet-counters/Exporters/JSONExporter.cs b/src/Tools/dotnet-counters/Exporters/JSONExporter.cs index 8abadbbeb..ec571ad7b 100644 --- a/src/Tools/dotnet-counters/Exporters/JSONExporter.cs +++ b/src/Tools/dotnet-counters/Exporters/JSONExporter.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.IO; using System.Text; +using Microsoft.Diagnostics.Monitoring.EventPipe; namespace Microsoft.Diagnostics.Tools.Counters.Exporters { @@ -72,10 +73,10 @@ namespace Microsoft.Diagnostics.Tools.Counters.Exporters } builder .Append("{ \"timestamp\": \"").Append(DateTime.Now.ToString("u")).Append("\", ") - .Append(" \"provider\": \"").Append(JsonEscape(payload.ProviderName)).Append("\", ") - .Append(" \"name\": \"").Append(JsonEscape(payload.DisplayName)).Append("\", ") - .Append(" \"tags\": \"").Append(JsonEscape(payload.Tags)).Append("\", ") - .Append(" \"counterType\": \"").Append(JsonEscape(payload.CounterType)).Append("\", ") + .Append(" \"provider\": \"").Append(JsonEscape(payload.Provider)).Append("\", ") + .Append(" \"name\": \"").Append(JsonEscape(payload.GetDisplay())).Append("\", ") + .Append(" \"tags\": \"").Append(JsonEscape(payload.Metadata)).Append("\", ") + .Append(" \"counterType\": \"").Append(JsonEscape(payload.CounterType.ToString())).Append("\", ") .Append(" \"value\": ").Append(payload.Value.ToString(CultureInfo.InvariantCulture)).Append(" },"); } } diff --git a/src/Tools/dotnet-counters/Program.cs b/src/Tools/dotnet-counters/Program.cs index ec79a2e6e..d1fde42e6 100644 --- a/src/Tools/dotnet-counters/Program.cs +++ b/src/Tools/dotnet-counters/Program.cs @@ -21,7 +21,7 @@ namespace Microsoft.Diagnostics.Tools.Counters internal static class Program { - private delegate Task CollectDelegate( + private delegate Task CollectDelegate( CancellationToken ct, List counter_list, string counters, @@ -37,7 +37,7 @@ namespace Microsoft.Diagnostics.Tools.Counters int maxTimeSeries, TimeSpan duration); - private delegate Task MonitorDelegate( + private delegate Task MonitorDelegate( CancellationToken ct, List counter_list, string counters, diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs index 46ae59463..e7b4ae375 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs @@ -57,11 +57,11 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests public IEnumerable Metrics => _metrics.Values; - public void Log(ICounterPayload metric) + public void Log(ICounterPayload payload) { - string key = CreateKey(metric); + string key = CreateKey(payload); - _metrics[key] = metric; + _metrics[key] = payload; // Complete the task source if the last expected key was removed. if (_expectedCounters.Remove(key) && _expectedCounters.Count == 0) diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs index 9634999aa..b55ed5a64 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs @@ -477,7 +477,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests // Add some variance between -5 and 5 milliseconds to simulate "real" timestamp _lastTimestamp = _lastTimestamp.Value.AddMilliseconds((10 * _random.NextDouble()) - 5); - return new CounterPayload( + return new EventCounterPayload( _lastTimestamp.Value, EventCounterConstants.RuntimeProviderName, EventCounterConstants.CpuUsageCounterName, @@ -486,6 +486,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests value, CounterType.Metric, actualInterval, + (int)_intervalSeconds, null); } } diff --git a/src/tests/dotnet-counters/CSVExporterTests.cs b/src/tests/dotnet-counters/CSVExporterTests.cs index 7c283ff4e..342d36748 100644 --- a/src/tests/dotnet-counters/CSVExporterTests.cs +++ b/src/tests/dotnet-counters/CSVExporterTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using Microsoft.Diagnostics.Tools.Counters; using Microsoft.Diagnostics.Tools.Counters.Exporters; +using Microsoft.Diagnostics.Monitoring.EventPipe; using Xunit; namespace DotnetCounters.UnitTests @@ -25,7 +26,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 100; i++) { - exporter.CounterPayloadReceived(new RatePayload("myProvider", "incrementingCounterOne", "Incrementing Counter One", "", "", i, 1, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new RatePayload("myProvider", "incrementingCounterOne", "Incrementing Counter One", string.Empty, string.Empty, i, 1, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -63,7 +64,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 10; i++) { - exporter.CounterPayloadReceived(new GaugePayload("myProvider", "counterOne", "Counter One", "", "", i, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new GaugePayload("myProvider", "counterOne", "Counter One", string.Empty, null, i, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -101,7 +102,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 100; i++) { - exporter.CounterPayloadReceived(new RatePayload("myProvider", "incrementingCounterOne", "Incrementing Counter One", "", "", i, 60, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new RatePayload("myProvider", "incrementingCounterOne", "Incrementing Counter One", string.Empty, null, i, 60, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -139,7 +140,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 100; i++) { - exporter.CounterPayloadReceived(new RatePayload("myProvider", "allocRateGen", "Allocation Rate Gen", "MB", "", i, 60, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new RatePayload("myProvider", "allocRateGen", "Allocation Rate Gen", "MB", string.Empty, i, 60, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); diff --git a/src/tests/dotnet-counters/JSONExporterTests.cs b/src/tests/dotnet-counters/JSONExporterTests.cs index 379f699be..d4daadbbc 100644 --- a/src/tests/dotnet-counters/JSONExporterTests.cs +++ b/src/tests/dotnet-counters/JSONExporterTests.cs @@ -6,7 +6,9 @@ using System.IO; using Microsoft.Diagnostics.Tools.Counters; using Microsoft.Diagnostics.Tools.Counters.Exporters; using Newtonsoft.Json; +using Microsoft.Diagnostics.Monitoring.EventPipe; using Xunit; +using System.Collections.Generic; #pragma warning disable CA1507 // Use nameof to express symbol names @@ -26,7 +28,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 10; i++) { - exporter.CounterPayloadReceived(new RatePayload("myProvider", "incrementingCounterOne", "Incrementing Counter One", "", "", 1, 1, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new RatePayload("myProvider", "incrementingCounterOne", "Incrementing Counter One", string.Empty, string.Empty, 1, 1, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -57,7 +59,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 10; i++) { - exporter.CounterPayloadReceived(new GaugePayload("myProvider", "counterOne", "Counter One", "", "", 1, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new GaugePayload("myProvider", "counterOne", "Counter One", string.Empty, string.Empty, 1, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -88,7 +90,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 20; i++) { - exporter.CounterPayloadReceived(new GaugePayload("myProvider", "heapSize", "Heap Size", "MB", "", i, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new GaugePayload("myProvider", "heapSize", "Heap Size", "MB", string.Empty, i, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -122,7 +124,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 20; i++) { - exporter.CounterPayloadReceived(new RatePayload("myProvider", "heapSize", "Heap Size", "MB", "", 0, 60, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new RatePayload("myProvider", "heapSize", "Heap Size", "MB", string.Empty, 0, 60, start + TimeSpan.FromSeconds(i)), false); } exporter.Stop(); @@ -210,7 +212,7 @@ namespace DotnetCounters.UnitTests DateTime start = DateTime.Now; for (int i = 0; i < 10; i++) { - exporter.CounterPayloadReceived(new PercentilePayload("myProvider", "counterOne", "Counter One", "", "f=abc,Percentile=50", 1, start + TimeSpan.FromSeconds(i)), false); + exporter.CounterPayloadReceived(new PercentilePayload("myProvider", "counterOne", "Counter One", "", "f=abc,Percentile=50", 1, start + TimeSpan.FromSeconds(1)), false); } exporter.Stop(); -- 2.34.1