diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs index 4047e11242..a045a343ec 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs @@ -13,39 +13,83 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { + [Flags] + public enum MetricType + { + EventCounter = 0x1, + Meter = 0x2, + All = 0xFF + } + + public sealed class MetricEventPipeProvider + { + public string Provider { get; set; } + + public float? IntervalSeconds { get; set; } + + public MetricType Type { get; set; } = MetricType.All; + } + public sealed class MetricSourceConfiguration : MonitoringSourceConfiguration { private readonly IList _eventPipeProviders; + public string SessionId { get; private set; } - public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable customProviderNames) + public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable eventCounterProviderNames) + : this(metricIntervalSeconds, CreateProviders(eventCounterProviderNames?.Any() == true ? eventCounterProviderNames : DefaultMetricProviders)) { - RequestRundown = false; - if (customProviderNames == null) - { - throw new ArgumentNullException(nameof(customProviderNames)); - } - MetricIntervalSeconds = metricIntervalSeconds.ToString(CultureInfo.InvariantCulture); + } - IEnumerable providers = null; - if (customProviderNames.Any()) + public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable providers, int maxHistograms = 20, int maxTimeSeries = 1000) + { + if (providers == null) { - providers = customProviderNames; + throw new ArgumentNullException(nameof(providers)); } - else + + RequestRundown = false; + + _eventPipeProviders = providers.Where(provider => provider.Type.HasFlag(MetricType.EventCounter)) + .Select((MetricEventPipeProvider provider) => new EventPipeProvider(provider.Provider, + EventLevel.Informational, + (long)ClrTraceEventParser.Keywords.None, + new Dictionary() + { + { + "EventCounterIntervalSec", (provider.IntervalSeconds ?? metricIntervalSeconds).ToString(CultureInfo.InvariantCulture) + } + })).ToList(); + + IEnumerable meterProviders = providers.Where(provider => provider.Type.HasFlag(MetricType.Meter)); + + if (meterProviders.Any()) { - providers = new[] { SystemRuntimeEventSourceName, MicrosoftAspNetCoreHostingEventSourceName, GrpcAspNetCoreServer }; - } + const long TimeSeriesValuesEventKeyword = 0x2; + string metrics = string.Join(',', meterProviders.Select(p => p.Provider)); + + SessionId = Guid.NewGuid().ToString(); - _eventPipeProviders = providers.Select((string provider) => new EventPipeProvider(provider, - EventLevel.Informational, - (long)ClrTraceEventParser.Keywords.None, - new Dictionary() - { - { "EventCounterIntervalSec", MetricIntervalSeconds } - })).ToList(); + EventPipeProvider metricsEventSourceProvider = + new EventPipeProvider(MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName, EventLevel.Informational, TimeSeriesValuesEventKeyword, + new Dictionary() + { + { "SessionId", SessionId }, + { "Metrics", metrics }, + { "RefreshInterval", metricIntervalSeconds.ToString(CultureInfo.InvariantCulture) }, + { "MaxTimeSeries", maxTimeSeries.ToString(CultureInfo.InvariantCulture) }, + { "MaxHistograms", maxHistograms.ToString(CultureInfo.InvariantCulture) } + } + ); + + _eventPipeProviders = _eventPipeProviders.Append(metricsEventSourceProvider).ToArray(); + } } - private string MetricIntervalSeconds { get; } + private static IEnumerable CreateProviders(IEnumerable providers) => + providers.Select(provider => new MetricEventPipeProvider { + Provider = provider, + Type = MetricType.EventCounter + }); public override IList GetProviders() => _eventPipeProviders; } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs index 00486370f5..ec1aec13d0 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs @@ -27,6 +27,7 @@ public abstract class MonitoringSourceConfiguration public const string TplEventSource = "System.Threading.Tasks.TplEventSource"; public const string SampleProfilerProviderName = "Microsoft-DotNETCore-SampleProfiler"; public const string EventPipeProviderName = "Microsoft-DotNETCore-EventPipe"; + public const string SystemDiagnosticsMetricsProviderName = "System.Diagnostics.Metrics"; public static IEnumerable DefaultMetricProviders => new[] { SystemRuntimeEventSourceName, MicrosoftAspNetCoreHostingEventSourceName, GrpcAspNetCoreServer }; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs index dd83243ef7..c065514796 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs @@ -38,12 +38,20 @@ public void AddFilter(string providerName, string[] counters) public IEnumerable GetProviders() => _enabledCounters.Keys; + public int IntervalSeconds => _intervalMilliseconds / 1000; + public bool IsIncluded(string providerName, string counterName, int intervalMilliseconds) { if (_intervalMilliseconds != intervalMilliseconds) { return false; } + + return IsIncluded(providerName, counterName); + } + + public bool IsIncluded(string providerName, string counterName) + { if (_enabledCounters.Count == 0) { return true; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs index 0108e722bc..187cbfc387 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs @@ -5,17 +5,15 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +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 { -#if NETSTANDARD - private static readonly IReadOnlyDictionary Empty = new ReadOnlyDictionary(new Dictionary(0)); -#else - private static readonly IReadOnlyDictionary Empty = System.Collections.Immutable.ImmutableDictionary.Empty; -#endif - public CounterPayload(DateTime timestamp, string provider, string name, @@ -24,7 +22,7 @@ public CounterPayload(DateTime timestamp, double value, CounterType counterType, float interval, - Dictionary metadata) + string metadata) { Timestamp = timestamp; Name = name; @@ -34,14 +32,27 @@ public CounterPayload(DateTime timestamp, CounterType = counterType; Provider = provider; Interval = interval; - Metadata = metadata ?? Empty; + Metadata = metadata; + EventType = EventType.Gauge; + } + + // Copied from dotnet-counters + public CounterPayload(string providerName, string name, string displayName, string displayUnits, 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; } + public string DisplayName { get; protected set; } public string Unit { get; } @@ -55,6 +66,81 @@ public CounterPayload(DateTime timestamp, public string Provider { get; } - public IReadOnlyDictionary Metadata { get; } + public string Metadata { get; } + + public EventType EventType { get; set; } + } + + internal class GaugePayload : CounterPayload + { + public GaugePayload(string providerName, string name, string displayName, string displayUnits, string metadata, double value, DateTime timestamp) : + base(providerName, name, displayName, displayUnits, metadata, value, timestamp, "Metric", EventType.Gauge) + { + // 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 CounterEndedPayload : CounterPayload + { + public CounterEndedPayload(string providerName, string name, string displayName, DateTime timestamp) + : base(providerName, name, displayName, string.Empty, null, 0.0, timestamp, "Metric", EventType.CounterEnded) + { + + } + } + + internal class RatePayload : CounterPayload + { + public RatePayload(string providerName, string name, string displayName, string displayUnits, string metadata, double value, double intervalSecs, DateTime timestamp) : + base(providerName, name, displayName, displayUnits, metadata, value, timestamp, "Rate", EventType.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 metadata, IEnumerable quantiles, DateTime timestamp) : + base(providerName, name, displayName, displayUnits, metadata, 0.0, timestamp, "Metric", 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 + { + public ErrorPayload(string errorMessage) : this(errorMessage, DateTime.UtcNow) + { + } + + public ErrorPayload(string errorMessage, DateTime timestamp) : + base(string.Empty, string.Empty, string.Empty, string.Empty, null, 0.0, timestamp, "Metric", EventType.Error) + { + ErrorMessage = errorMessage; + } + + public string ErrorMessage { get; private set; } + } + + internal enum EventType : int + { + Rate, + Gauge, + Histogram, + Error, + CounterEnded } } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterUtilities.cs new file mode 100644 index 0000000000..26ead38882 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterUtilities.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe +{ + internal static class CounterUtilities + { + //The metadata payload is formatted as a string of comma separated key:value pairs. + //This limitation means that metadata values cannot include commas; otherwise, the + //metadata will be parsed incorrectly. If a value contains a comma, then all metadata + //is treated as invalid and excluded from the payload. + public static IDictionary GetMetadata(string metadataPayload, char kvSeparator = ':') + { + var metadataDict = new Dictionary(); + + ReadOnlySpan metadata = metadataPayload; + + while (!metadata.IsEmpty) { + int commaIndex = metadata.IndexOf(','); + + ReadOnlySpan kvPair; + + if (commaIndex < 0) { + kvPair = metadata; + metadata = default; + } + else { + kvPair = metadata[..commaIndex]; + metadata = metadata.Slice(commaIndex + 1); + } + + int colonIndex = kvPair.IndexOf(kvSeparator); + if (colonIndex < 0) { + metadataDict.Clear(); + break; + } + + string metadataKey = kvPair[..colonIndex].ToString(); + string metadataValue = kvPair.Slice(colonIndex + 1).ToString(); + metadataDict[metadataKey] = metadataValue; + } + + return metadataDict; + } + + public static string AppendPercentile(string tags, double quantile) => AppendPercentile(tags, FormattableString.Invariant($"Percentile={(int)(100 * quantile)}")); + + private static string AppendPercentile(string tags, string percentile) => string.IsNullOrEmpty(tags) ? percentile : string.Concat(tags, ",", percentile); + } +} + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs index b492a629bb..bec5523d51 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs @@ -33,8 +33,17 @@ internal interface ICounterPayload DateTime Timestamp { get; } + /// + /// The interval between counters. Note this is the actual measure of time elapsed, not the requested interval. + /// float Interval { get; } - IReadOnlyDictionary Metadata { get; } + /// + /// Optional metadata for counters. Note that normal counters use ':' as a separator character, while System.Diagnostics.Metrics use ';'. + /// We do not immediately convert string to Dictionary, since dotnet-counters does not need this conversion. + /// + string Metadata { get; } + + EventType EventType { get; set; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICountersLogger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICountersLogger.cs index 8a522c9fe9..7a84c607e5 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICountersLogger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICountersLogger.cs @@ -2,9 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,10 +9,9 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { internal interface ICountersLogger { - //TODO Consider making these async. - void Log(ICounterPayload counter); - void PipelineStarted(); - void PipelineStopped(); + + Task PipelineStarted(CancellationToken token); + Task PipelineStopped(CancellationToken token); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs similarity index 64% rename from src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs rename to src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs index 14cdfe5b9f..1dfad989c7 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipeline.cs @@ -6,18 +6,20 @@ using Microsoft.Diagnostics.Tracing; using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.EventPipe { - internal class EventCounterPipeline : EventSourcePipeline + internal class MetricsPipeline : EventSourcePipeline { private readonly IEnumerable _loggers; private readonly CounterFilter _filter; + private string _sessionId; - public EventCounterPipeline(DiagnosticsClient client, - EventPipeCounterPipelineSettings settings, + public MetricsPipeline(DiagnosticsClient client, + MetricsPipelineSettings settings, IEnumerable loggers) : base(client, settings) { _loggers = loggers ?? throw new ArgumentNullException(nameof(loggers)); @@ -38,18 +40,28 @@ public EventCounterPipeline(DiagnosticsClient client, protected override MonitoringSourceConfiguration CreateConfiguration() { - return new MetricSourceConfiguration(Settings.CounterIntervalSeconds, _filter.GetProviders()); + var config = new MetricSourceConfiguration(Settings.CounterIntervalSeconds, Settings.CounterGroups.Select((EventPipeCounterGroup counterGroup) => new MetricEventPipeProvider + { + Provider = counterGroup.ProviderName, + IntervalSeconds = counterGroup.IntervalSeconds, + Type = (MetricType)counterGroup.Type + }), + Settings.MaxHistograms, Settings.MaxTimeSeries); + + _sessionId = config.SessionId; + + return config; } protected override async Task OnEventSourceAvailable(EventPipeEventSource eventSource, Func stopSessionAsync, CancellationToken token) { - ExecuteCounterLoggerAction((metricLogger) => metricLogger.PipelineStarted()); + await ExecuteCounterLoggerActionAsync((metricLogger) => metricLogger.PipelineStarted(token)); eventSource.Dynamic.All += traceEvent => { try { - if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload counterPayload)) + if (traceEvent.TryGetCounterPayload(_filter, _sessionId, out ICounterPayload counterPayload)) { ExecuteCounterLoggerAction((metricLogger) => metricLogger.Log(counterPayload)); } @@ -67,7 +79,21 @@ protected override async Task OnEventSourceAvailable(EventPipeEventSource eventS await sourceCompletedTaskSource.Task; - ExecuteCounterLoggerAction((metricLogger) => metricLogger.PipelineStopped()); + await ExecuteCounterLoggerActionAsync((metricLogger) => metricLogger.PipelineStopped(token)); + } + + private async Task ExecuteCounterLoggerActionAsync(Func action) + { + foreach (ICountersLogger logger in _loggers) + { + try + { + await action(logger); + } + catch (ObjectDisposedException) + { + } + } } private void ExecuteCounterLoggerAction(Action action) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs similarity index 65% rename from src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs rename to src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs index ffcb93924a..f0c9d6e64e 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/MetricsPipelineSettings.cs @@ -8,18 +8,35 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { - internal class EventPipeCounterPipelineSettings : EventSourcePipelineSettings + internal class MetricsPipelineSettings : EventSourcePipelineSettings { public EventPipeCounterGroup[] CounterGroups { get; set; } //Do not use TimeSpan here since we may need to synchronize this pipeline interval //with a different session and want to make sure the values are identical. public float CounterIntervalSeconds { get; set; } + + public int MaxHistograms { get; set; } + + public int MaxTimeSeries { get; set; } + } + + [Flags] + internal enum CounterGroupType + { + EventCounter = 0x1, + Meter = 0x2, + All = 0xFF } internal class EventPipeCounterGroup { public string ProviderName { get; set; } + public string[] CounterNames { get; set; } + + public CounterGroupType Type { get; set; } = CounterGroupType.All; + + public float? IntervalSeconds { get; set; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs index f0ec5c6310..34255c6d34 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs @@ -5,12 +5,14 @@ using Microsoft.Diagnostics.Tracing; using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; namespace Microsoft.Diagnostics.Monitoring.EventPipe { internal static class TraceEventExtensions { - public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, out ICounterPayload payload) + public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, string sessionId, out ICounterPayload payload) { payload = null; @@ -23,7 +25,7 @@ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilte string series = payloadFields["Series"].ToString(); string counterName = payloadFields["Name"].ToString(); - Dictionary metadataDict = GetMetadata(payloadFields["Metadata"].ToString()); + string metadata = payloadFields["Metadata"].ToString(); //CONSIDER //Concurrent counter sessions do not each get a separate interval. Instead the payload @@ -57,6 +59,7 @@ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilte // 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( traceEvent.TimeStamp, traceEvent.ProviderName, @@ -65,53 +68,265 @@ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilte value, counterType, intervalSec, - metadataDict); + metadata); + return true; } + if (sessionId != null && MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName.Equals(traceEvent.ProviderName)) + { + if (traceEvent.EventName == "BeginInstrumentReporting") + { + // Do we want to log something for this? + //HandleBeginInstrumentReporting(traceEvent); + } + if (traceEvent.EventName == "HistogramValuePublished") + { + HandleHistogram(traceEvent, filter, sessionId, out payload); + } + else if (traceEvent.EventName == "GaugeValuePublished") + { + HandleGauge(traceEvent, filter, sessionId, out payload); + } + else if (traceEvent.EventName == "CounterRateValuePublished") + { + HandleCounterRate(traceEvent, filter, sessionId, out payload); + } + else if (traceEvent.EventName == "TimeSeriesLimitReached") + { + HandleTimeSeriesLimitReached(traceEvent, sessionId, out payload); + } + else if (traceEvent.EventName == "HistogramLimitReached") + { + HandleHistogramLimitReached(traceEvent, sessionId, out payload); + } + else if (traceEvent.EventName == "Error") + { + HandleError(traceEvent, sessionId, out payload); + } + else if (traceEvent.EventName == "ObservableInstrumentCallbackError") + { + HandleObservableInstrumentCallbackError(traceEvent, sessionId, out payload); + } + else if (traceEvent.EventName == "MultipleSessionsNotSupportedError") + { + HandleMultipleSessionsNotSupportedError(traceEvent, sessionId, out payload); + } + + return payload != null; + } + return false; } - //The metadata payload is formatted as a string of comma separated key:value pairs. - //This limitation means that metadata values cannot include commas; otherwise, the - //metadata will be parsed incorrectly. If a value contains a comma, then all metadata - //is treated as invalid and excluded from the payload. - internal static Dictionary GetMetadata(string metadataPayload) + private static void HandleGauge(TraceEvent obj, CounterFilter filter, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + + if (payloadSessionId != sessionId) + { + return; + } + + 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 (!filter.IsIncluded(meterName, instrumentName)) + { + return; + } + + // 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)) + { + payload = new GaugePayload(meterName, instrumentName, null, unit, tags, lastValue, obj.TimeStamp); + } + else + { + // for observable instruments we assume the lack of data is meaningful and remove it from the UI + // this happens when the Gauge callback function throws an exception. + payload = new CounterEndedPayload(meterName, instrumentName, null, obj.TimeStamp); + } + } + + private static void HandleCounterRate(TraceEvent traceEvent, CounterFilter filter, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)traceEvent.PayloadValue(0); + + if (payloadSessionId != sessionId) + { + return; + } + + string meterName = (string)traceEvent.PayloadValue(1); + //string meterVersion = (string)obj.PayloadValue(2); + string instrumentName = (string)traceEvent.PayloadValue(3); + string unit = (string)traceEvent.PayloadValue(4); + string tags = (string)traceEvent.PayloadValue(5); + string rateText = (string)traceEvent.PayloadValue(6); + + if (!filter.IsIncluded(meterName, instrumentName)) + { + return; + } + + if (double.TryParse(rateText, NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out double rate)) + { + payload = new RatePayload(meterName, instrumentName, null, unit, tags, rate, filter.IntervalSeconds, traceEvent.TimeStamp); + } + else + { + // for observable instruments we assume the lack of data is meaningful and remove it from the UI + // this happens when the ObservableCounter callback function throws an exception. + payload = new CounterEndedPayload(meterName, instrumentName, null, traceEvent.TimeStamp); + } + } + + private static void HandleHistogram(TraceEvent obj, CounterFilter filter, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + if (payloadSessionId != sessionId) + { + return; + } + + 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 (!filter.IsIncluded(meterName, instrumentName)) + { + return; + } + + //Note quantiles can be empty. + IList quantiles = ParseQuantiles(quantilesText); + payload = new PercentilePayload(meterName, instrumentName, null, unit, tags, quantiles, obj.TimeStamp); + } + + + + private static void HandleHistogramLimitReached(TraceEvent obj, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + + if (payloadSessionId != 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."; + + payload = new ErrorPayload(errorMessage); + } + + private static void HandleTimeSeriesLimitReached(TraceEvent obj, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + + if (payloadSessionId != 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."; + + payload = new ErrorPayload(errorMessage, obj.TimeStamp); + } + + private static void HandleError(TraceEvent obj, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + string error = (string)obj.PayloadValue(1); + if (sessionId != payloadSessionId) + { + return; + } + + string errorMessage = "Error reported from target process:" + Environment.NewLine + error; + + payload = new ErrorPayload(errorMessage, obj.TimeStamp); + } + + private static void HandleMultipleSessionsNotSupportedError(TraceEvent obj, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + if (payloadSessionId == 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; + } + else + { + string errorMessage = "Error: Another metrics collection session is already in progress for the target process, perhaps from another tool? " + Environment.NewLine + + "Concurrent sessions are not supported."; + + payload = new ErrorPayload(errorMessage, obj.TimeStamp); + } + } + + private static void HandleObservableInstrumentCallbackError(TraceEvent obj, string sessionId, out ICounterPayload payload) { - var metadataDict = new Dictionary(); + payload = null; - ReadOnlySpan metadata = metadataPayload; + string payloadSessionId = (string)obj.PayloadValue(0); + string error = (string)obj.PayloadValue(1); - while (!metadata.IsEmpty) + if (payloadSessionId != sessionId) { - int commaIndex = metadata.IndexOf(','); + return; + } - ReadOnlySpan kvPair; + string errorMessage = "Exception thrown from an observable instrument callback in the target process:" + Environment.NewLine + + error; - if (commaIndex < 0) + payload = new ErrorPayload(errorMessage, obj.TimeStamp); + } + + private static IList ParseQuantiles(string quantileList) + { + string[] quantileParts = quantileList.Split(';', StringSplitOptions.RemoveEmptyEntries); + var quantiles = new List(); + foreach (string quantile in quantileParts) + { + string[] keyValParts = quantile.Split('=', StringSplitOptions.RemoveEmptyEntries); + if (keyValParts.Length != 2) { - kvPair = metadata; - metadata = default; + continue; } - else + if (!double.TryParse(keyValParts[0], out double key)) { - kvPair = metadata[..commaIndex]; - metadata = metadata.Slice(commaIndex + 1); + continue; } - - int colonIndex = kvPair.IndexOf(':'); - if (colonIndex < 0) + if (!double.TryParse(keyValParts[1], out double val)) { - metadataDict.Clear(); - break; + continue; } - - string metadataKey = kvPair[..colonIndex].ToString(); - string metadataValue = kvPair.Slice(colonIndex + 1).ToString(); - metadataDict[metadataKey] = metadataValue; + quantiles.Add(new Quantile(key, val)); } - - return metadataDict; + return quantiles; } private static int GetInterval(string series) 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 19fce8c296..21ebf73be1 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj @@ -42,5 +42,7 @@ + + diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs index 622171d07c..1bc46fc7b0 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs @@ -59,7 +59,7 @@ public IReadOnlyDictionary> GetProviderEvent public bool HasSatisfiedCondition(TraceEvent traceEvent) { // Filter to the counter of interest before forwarding to the implementation - if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload payload)) + if (traceEvent.TryGetCounterPayload(_filter, null, out ICounterPayload payload)) { return _impl.HasSatisfiedCondition(payload); } diff --git a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj index d1157e98da..d0352a014e 100644 --- a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj +++ b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj @@ -28,6 +28,7 @@ + diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs index c392582ce2..5a05bd0f8d 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterPipelineUnitTests.cs @@ -71,13 +71,9 @@ public void Log(ICounterPayload metric) } } - public void PipelineStarted() - { - } + public Task PipelineStarted(CancellationToken token) => Task.CompletedTask; - public void PipelineStopped() - { - } + public Task PipelineStopped(CancellationToken token) => Task.CompletedTask; private static string CreateKey(ICounterPayload payload) { @@ -107,7 +103,7 @@ public async Task TestCounterEventPipeline(TestConfiguration config) { var client = new DiagnosticsClient(testRunner.Pid); - await using EventCounterPipeline pipeline = new EventCounterPipeline(client, new EventPipeCounterPipelineSettings + await using MetricsPipeline pipeline = new MetricsPipeline(client, new MetricsPipelineSettings { Duration = Timeout.InfiniteTimeSpan, CounterGroups = new[] diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs index 80f05d4911..769500015d 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs @@ -502,7 +502,7 @@ public void ValidateMetadataParsing_Success() const string value1 = "V1"; const string key2 = "K2"; const string value2 = "V:2"; - Dictionary metadataDict = TraceEventExtensions.GetMetadata($"{key1}:{value1},{key2}:{value2}"); + IDictionary metadataDict = CounterUtilities.GetMetadata($"{key1}:{value1},{key2}:{value2}"); Assert.Equal(2, metadataDict.Count); Assert.Equal(value1, metadataDict[key1]); @@ -518,7 +518,7 @@ public void ValidateMetadataParsing_Success() [InlineData("K1")] public void ValidateMetadataParsing_Failure(string invalidMetadata) { - Dictionary metadataDict = TraceEventExtensions.GetMetadata(invalidMetadata); + IDictionary metadataDict = CounterUtilities.GetMetadata(invalidMetadata); Assert.Empty(metadataDict); }