diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 0212e2b3e6..b639323729 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -12,9 +12,9 @@ https://github.com/microsoft/clrmd a64d9ac11086f28fbd4b2b2337c19be7826fbfa9 - + https://github.com/dotnet/source-build-reference-packages - 7ec4b0aabc55efa11515c987fb0251d39c29f06c + bed0fd20a5a55843c1f04ef195ef2fd7171ae715 diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs index 4047e11242..1c3891f123 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs @@ -16,6 +16,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe public sealed class MetricSourceConfiguration : MonitoringSourceConfiguration { private readonly IList _eventPipeProviders; + public string SessionId { get; private set; } public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable customProviderNames) { @@ -45,6 +46,37 @@ public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable customProviderNames, int maxHistograms, int maxTimeSeries) : this(metricIntervalSeconds, customProviderNames) + { + const long TimeSeriesValues = 0x2; + StringBuilder metrics = new StringBuilder(); + foreach (string provider in customProviderNames) + { + if (metrics.Length != 0) + { + metrics.Append(","); + } + + metrics.Append(provider); + } + + SessionId = Guid.NewGuid().ToString(); + + EventPipeProvider metricsEventSourceProvider = + new EventPipeProvider("System.Diagnostics.Metrics", EventLevel.Informational, TimeSeriesValues, + new Dictionary() + { + { "SessionId", SessionId }, + { "Metrics", metrics.ToString() }, + { "RefreshInterval", MetricIntervalSeconds.ToString() }, + { "MaxTimeSeries", maxTimeSeries.ToString() }, + { "MaxHistograms", maxHistograms.ToString() } + } + ); + + _eventPipeProviders = _eventPipeProviders.Append(metricsEventSourceProvider).ToArray(); + } + private string MetricIntervalSeconds { get; } public override IList GetProviders() => _eventPipeProviders; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs index dd83243ef7..9ad08494d4 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs @@ -44,6 +44,12 @@ public bool IsIncluded(string providerName, string counterName, int intervalMill { 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..fc9f8a9485 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterPayload.cs @@ -8,7 +8,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { - internal class CounterPayload : ICounterPayload + public class CounterPayload : ICounterPayload { #if NETSTANDARD private static readonly IReadOnlyDictionary Empty = new ReadOnlyDictionary(new Dictionary(0)); @@ -35,13 +35,26 @@ public CounterPayload(DateTime timestamp, Provider = provider; Interval = interval; Metadata = metadata ?? Empty; + EventType = EventType.Gauge; + } + + // Copied from dotnet-counters + public CounterPayload(string providerName, string name, string displayName, string displayUnits, Dictionary metadata, double value, DateTime timestamp, string type, EventType eventType) + { + Provider = providerName; + Name = name; + Metadata = metadata ?? Empty; + 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; } @@ -56,5 +69,63 @@ public CounterPayload(DateTime timestamp, public string Provider { get; } public IReadOnlyDictionary Metadata { get; } + + public EventType EventType { get; set; } + + } + + public class GaugePayload : CounterPayload + { + public GaugePayload(string providerName, string name, string displayName, string displayUnits, Dictionary 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; + } + } + + public class RatePayload : CounterPayload + { + public RatePayload(string providerName, string name, string displayName, string displayUnits, Dictionary 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})"; + } + } + + public class PercentilePayload : CounterPayload + { + public PercentilePayload(string providerName, string name, string displayName, string displayUnits, Dictionary metadata, double val, DateTime timestamp) : + base(providerName, name, displayName, displayUnits, metadata, val, 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; + } + } + + public class ErrorPayload : CounterPayload + { + public ErrorPayload(string providerName, string name, string displayName, string displayUnits, Dictionary metadata, double val, DateTime timestamp, string errorMessage) : + base(providerName, name, displayName, displayUnits, metadata, val, timestamp, "Metric", EventType.Error) + { + ErrorMessage = errorMessage; + } + + public string ErrorMessage { get; private set; } + } + + // If keep this, should probably put it somewhere else + public enum EventType : int + { + Rate, + Gauge, + Histogram, + Error } } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs index 14cdfe5b9f..280c552f58 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs @@ -15,6 +15,7 @@ internal class EventCounterPipeline : EventSourcePipeline _loggers; private readonly CounterFilter _filter; + private string _sessionId; public EventCounterPipeline(DiagnosticsClient client, EventPipeCounterPipelineSettings settings, @@ -38,7 +39,11 @@ public EventCounterPipeline(DiagnosticsClient client, protected override MonitoringSourceConfiguration CreateConfiguration() { - return new MetricSourceConfiguration(Settings.CounterIntervalSeconds, _filter.GetProviders()); + var config = new MetricSourceConfiguration(Settings.CounterIntervalSeconds, _filter.GetProviders(), Settings.MaxHistograms, Settings.MaxTimeSeries); + + _sessionId = config.SessionId; + + return config; } protected override async Task OnEventSourceAvailable(EventPipeEventSource eventSource, Func stopSessionAsync, CancellationToken token) @@ -49,7 +54,7 @@ protected override async Task OnEventSourceAvailable(EventPipeEventSource eventS { try { - if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload counterPayload)) + if (traceEvent.TryGetCounterPayload(_filter, _sessionId, out List counterPayload)) { ExecuteCounterLoggerAction((metricLogger) => metricLogger.Log(counterPayload)); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs index ffcb93924a..9aa3855634 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventPipeCounterPipelineSettings.cs @@ -15,6 +15,10 @@ internal class EventPipeCounterPipelineSettings : EventSourcePipelineSettings //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; } } internal class EventPipeCounterGroup diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs index b492a629bb..cc27e69d9b 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICounterPayload.cs @@ -8,7 +8,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { - internal enum CounterType + public enum CounterType { //Same as average or mean Metric, @@ -17,7 +17,7 @@ internal enum CounterType Rate } - internal interface ICounterPayload + public interface ICounterPayload { string Name { get; } @@ -36,5 +36,7 @@ internal interface ICounterPayload float Interval { get; } IReadOnlyDictionary Metadata { get; } + + public 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..b0ddf1f753 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICountersLogger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/ICountersLogger.cs @@ -14,7 +14,7 @@ internal interface ICountersLogger { //TODO Consider making these async. - void Log(ICounterPayload counter); + void Log(List counter); void PipelineStarted(); void PipelineStopped(); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs index f0ec5c6310..96c531f3e7 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs @@ -5,14 +5,15 @@ using Microsoft.Diagnostics.Tracing; using System; using System.Collections.Generic; +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 List payload) { - payload = null; + payload = new List(); if ("EventCounters".Equals(traceEvent.EventName)) { @@ -57,7 +58,8 @@ 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( + + payload.Add(new CounterPayload( traceEvent.TimeStamp, traceEvent.ProviderName, counterName, displayName, @@ -65,13 +67,247 @@ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilte value, counterType, intervalSec, - metadataDict); + metadataDict)); + return true; } + if (sessionId != null && "System.Diagnostics.Metrics".Equals(traceEvent.ProviderName)) + { + + ICounterPayload individualPayload = null; + + 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 individualPayload); + } + else if (traceEvent.EventName == "CounterRateValuePublished") + { + HandleCounterRate(traceEvent, filter, sessionId, out individualPayload); + } + else if (traceEvent.EventName == "TimeSeriesLimitReached") + { + HandleTimeSeriesLimitReached(traceEvent, sessionId, out individualPayload); + } + else if (traceEvent.EventName == "HistogramLimitReached") + { + HandleHistogramLimitReached(traceEvent, sessionId, out individualPayload); + } + else if (traceEvent.EventName == "Error") + { + HandleError(traceEvent, sessionId, out individualPayload); + } + else if (traceEvent.EventName == "ObservableInstrumentCallbackError") + { + HandleObservableInstrumentCallbackError(traceEvent, sessionId, out individualPayload); + } + else if (traceEvent.EventName == "MultipleSessionsNotSupportedError") + { + HandleMultipleSessionsNotSupportedError(traceEvent, sessionId, out individualPayload); + } + + if (null != individualPayload) + { + payload.Add(individualPayload); + } + + return null != payload && payload.Any(); + } + return false; } + public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, out List payload) + { + return TryGetCounterPayload(traceEvent, filter, null, out payload); + } + + 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); + + Dictionary metadataDict = GetMetadata(tags); + + 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, out double lastValue)) + { + payload = new GaugePayload(meterName, instrumentName, null, unit, metadataDict, lastValue, 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); + + Dictionary metadataDict = GetMetadata(tags); + + if (double.TryParse(rateText, out double rate)) + { + payload = new RatePayload(meterName, instrumentName, null, unit, metadataDict, rate, 10, traceEvent.TimeStamp); + } + } + + private static void HandleHistogram(TraceEvent obj, CounterFilter filter, string sessionId, out List payload) + { + payload = new List(); + + 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; + } + + KeyValuePair[] quantiles = ParseQuantiles(quantilesText); + foreach ((double key, double val) in quantiles) + { + Dictionary metadataDict = GetMetadata(tags); + metadataDict.Add("quantile", key.ToString()); + payload.Add(new PercentilePayload(meterName, instrumentName, null, unit, metadataDict, val, 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(string.Empty, string.Empty, string.Empty, string.Empty, new(), 0, obj.TimeStamp, 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(string.Empty, string.Empty, string.Empty, string.Empty, new(), 0, obj.TimeStamp, errorMessage); + } + + 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(string.Empty, string.Empty, string.Empty, string.Empty, new(), 0, obj.TimeStamp, errorMessage); + } + + 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(string.Empty, string.Empty, string.Empty, string.Empty, new(), 0, obj.TimeStamp, errorMessage); + } + } + + private static void HandleObservableInstrumentCallbackError(TraceEvent obj, string sessionId, out ICounterPayload payload) + { + payload = null; + + string payloadSessionId = (string)obj.PayloadValue(0); + string error = (string)obj.PayloadValue(1); + + if (payloadSessionId != sessionId) + { + return; + } + + string errorMessage = "Exception thrown from an observable instrument callback in the target process:" + Environment.NewLine + + error; + + payload = new ErrorPayload(string.Empty, string.Empty, string.Empty, string.Empty, new(), 0, obj.TimeStamp, errorMessage); + } + + //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 @@ -114,6 +350,30 @@ internal static Dictionary GetMetadata(string metadataPayload) return metadataDict; } + private static KeyValuePair[] ParseQuantiles(string quantileList) + { + string[] quantileParts = quantileList.Split(';', StringSplitOptions.RemoveEmptyEntries); + List> quantiles = new List>(); + foreach (string quantile in quantileParts) + { + string[] keyValParts = quantile.Split('=', StringSplitOptions.RemoveEmptyEntries); + if (keyValParts.Length != 2) + { + continue; + } + if (!double.TryParse(keyValParts[0], out double key)) + { + continue; + } + if (!double.TryParse(keyValParts[1], out double val)) + { + continue; + } + quantiles.Add(new KeyValuePair(key, val)); + } + return quantiles.ToArray(); + } + private static int GetInterval(string series) { const string comparison = "Interval="; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs index 622171d07c..51fd9f3de8 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs @@ -59,9 +59,9 @@ 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, out List payload)) { - return _impl.HasSatisfiedCondition(payload); + return _impl.HasSatisfiedCondition(payload[0]); // Need to check if this is safe - in theory just want the first (and only) result } return false; }