From 91e140db2b703cd4c1cdb760244ef76d87c4a4e3 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Tue, 10 Aug 2021 22:03:42 -0700 Subject: [PATCH 1/6] Initial trace event trigger implementation and tests. --- eng/Versions.props | 1 + .../Counters/CounterFilter.cs | 13 +- .../Counters/EventCounterPipeline.cs | 65 +-- .../Counters/TraceEventExtensions.cs | 78 +++ ...ft.Diagnostics.Monitoring.EventPipe.csproj | 1 + .../EventCounter/EventCounterTrigger.cs | 71 +++ .../EventCounterTriggerFactory.cs | 21 + .../EventCounter/EventCounterTriggerImpl.cs | 92 ++++ .../EventCounterTriggerSettings.cs | 95 ++++ .../Triggers/ITraceEventTrigger.cs | 31 ++ .../Triggers/ITraceEventTriggerFactory.cs | 18 + .../Pipelines/EventPipeTriggerPipeline.cs | 94 ++++ .../EventPipeTriggerPipelineSettings.cs | 26 + .../Pipelines/TraceEventTriggerPipeline.cs | 111 ++++ src/tests/EventPipeTracee/Program.cs | 20 +- .../EventCounterConstants.cs | 17 + .../EventCounterTriggerTests.cs | 489 ++++++++++++++++++ 17 files changed, 1176 insertions(+), 67 deletions(-) create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerFactory.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTriggerFactory.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipelineSettings.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs create mode 100644 src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs create mode 100644 src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs diff --git a/eng/Versions.props b/eng/Versions.props index 0882148140..419da4449d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -45,6 +45,7 @@ 5.0.1 2.0.0-beta1.20468.1 2.0.0-beta1.20074.1 + 5.0.0 4.5.4 4.7.2 4.7.1 diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs index acfb408148..df2756c3e6 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterFilter.cs @@ -11,13 +11,16 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe internal sealed class CounterFilter { private Dictionary> _enabledCounters; + private int _intervalMilliseconds; - public static CounterFilter AllCounters { get; } = new CounterFilter(); + public static CounterFilter AllCounters(int counterIntervalSeconds) + => new CounterFilter(counterIntervalSeconds); - public CounterFilter() + public CounterFilter(int intervalSeconds) { //Provider names are not case sensitive, but counter names are. _enabledCounters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _intervalMilliseconds = intervalSeconds * 1000; } // Called when we want to enable all counters under a provider name. @@ -33,8 +36,12 @@ public void AddFilter(string providerName, string[] counters) public IEnumerable GetProviders() => _enabledCounters.Keys; - public bool IsIncluded(string providerName, string counterName) + public bool IsIncluded(string providerName, string counterName, int interval) { + if (_intervalMilliseconds != interval) + { + return false; + } if (_enabledCounters.Count == 0) { return true; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs index a778091235..12beff51b6 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/EventCounterPipeline.cs @@ -24,7 +24,7 @@ public EventCounterPipeline(DiagnosticsClient client, if (settings.CounterGroups.Length > 0) { - _filter = new CounterFilter(); + _filter = new CounterFilter(CounterIntervalSeconds); foreach (var counterGroup in settings.CounterGroups) { _filter.AddFilter(counterGroup.ProviderName, counterGroup.CounterNames); @@ -32,7 +32,7 @@ public EventCounterPipeline(DiagnosticsClient client, } else { - _filter = CounterFilter.AllCounters; + _filter = CounterFilter.AllCounters(CounterIntervalSeconds); } } @@ -49,56 +49,8 @@ protected override async Task OnEventSourceAvailable(EventPipeEventSource eventS { try { - // Metrics - if (traceEvent.EventName.Equals("EventCounters")) + if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload counterPayload)) { - IDictionary payloadVal = (IDictionary)(traceEvent.PayloadValue(0)); - IDictionary payloadFields = (IDictionary)(payloadVal["Payload"]); - - //Make sure we are part of the requested series. If multiple clients request metrics, all of them get the metrics. - string series = payloadFields["Series"].ToString(); - if (GetInterval(series) != CounterIntervalSeconds * 1000) - { - return; - } - - string counterName = payloadFields["Name"].ToString(); - if (!_filter.IsIncluded(traceEvent.ProviderName, counterName)) - { - return; - } - - float intervalSec = (float)payloadFields["IntervalSec"]; - string displayName = payloadFields["DisplayName"].ToString(); - string displayUnits = payloadFields["DisplayUnits"].ToString(); - double value = 0; - CounterType counterType = CounterType.Metric; - - if (payloadFields["CounterType"].Equals("Mean")) - { - value = (double)payloadFields["Mean"]; - } - else if (payloadFields["CounterType"].Equals("Sum")) - { - counterType = CounterType.Rate; - value = (double)payloadFields["Increment"]; - if (string.IsNullOrEmpty(displayUnits)) - { - displayUnits = "count"; - } - //TODO Should we make these /sec like the dotnet-counters tool? - } - - // Note that dimensional data such as pod and namespace are automatically added in prometheus and azure monitor scenarios. - // We no longer added it here. - var counterPayload = new CounterPayload(traceEvent.TimeStamp, - traceEvent.ProviderName, - counterName, displayName, - displayUnits, - value, - counterType, - intervalSec); - ExecuteCounterLoggerAction((metricLogger) => metricLogger.Log(counterPayload)); } } @@ -118,17 +70,6 @@ protected override async Task OnEventSourceAvailable(EventPipeEventSource eventS ExecuteCounterLoggerAction((metricLogger) => metricLogger.PipelineStopped()); } - private static int GetInterval(string series) - { - const string comparison = "Interval="; - int interval = 0; - if (series.StartsWith(comparison, StringComparison.OrdinalIgnoreCase)) - { - int.TryParse(series.Substring(comparison.Length), out interval); - } - return interval; - } - private void ExecuteCounterLoggerAction(Action action) { foreach (ICountersLogger logger in _loggers) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs new file mode 100644 index 0000000000..edf73a89e6 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/TraceEventExtensions.cs @@ -0,0 +1,78 @@ +// 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 Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe +{ + internal static class TraceEventExtensions + { + public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, out ICounterPayload payload) + { + payload = null; + + if ("EventCounters".Equals(traceEvent.EventName)) + { + IDictionary payloadVal = (IDictionary)(traceEvent.PayloadValue(0)); + IDictionary payloadFields = (IDictionary)(payloadVal["Payload"]); + + //Make sure we are part of the requested series. If multiple clients request metrics, all of them get the metrics. + string series = payloadFields["Series"].ToString(); + string counterName = payloadFields["Name"].ToString(); + if (!filter.IsIncluded(traceEvent.ProviderName, counterName, GetInterval(series))) + { + return false; + } + + float intervalSec = (float)payloadFields["IntervalSec"]; + string displayName = payloadFields["DisplayName"].ToString(); + string displayUnits = payloadFields["DisplayUnits"].ToString(); + double value = 0; + CounterType counterType = CounterType.Metric; + + if (payloadFields["CounterType"].Equals("Mean")) + { + value = (double)payloadFields["Mean"]; + } + else if (payloadFields["CounterType"].Equals("Sum")) + { + counterType = CounterType.Rate; + value = (double)payloadFields["Increment"]; + if (string.IsNullOrEmpty(displayUnits)) + { + displayUnits = "count"; + } + //TODO Should we make these /sec like the dotnet-counters tool? + } + + // 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, + counterName, displayName, + displayUnits, + value, + counterType, + intervalSec); + return true; + } + + return false; + } + + private static int GetInterval(string series) + { + const string comparison = "Interval="; + int interval = 0; + if (series.StartsWith(comparison, StringComparison.OrdinalIgnoreCase)) + { + int.TryParse(series.Substring(comparison.Length), out interval); + } + return interval; + } + } +} 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 f24f14d494..efc358643f 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs new file mode 100644 index 0000000000..9ba7c0104c --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs @@ -0,0 +1,71 @@ +// 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 Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter +{ + /// + /// Trigger that detects when the specified event source counter value is held + /// above, below, or between threshold values for a specified duration of time. + /// + internal sealed class EventCounterTrigger : + ITraceEventTrigger + { + private readonly CounterFilter _filter; + private readonly EventCounterTriggerImpl _impl; + private readonly string _providerName; + + public EventCounterTrigger(EventCounterTriggerSettings settings) + { + if (null == settings) + { + throw new ArgumentNullException(nameof(settings)); + } + + Validate(settings); + + _filter = new CounterFilter(settings.CounterIntervalSeconds); + _filter.AddFilter(settings.ProviderName, new string[] { settings.CounterName }); + + _impl = new(settings); + + _providerName = settings.ProviderName; + } + + public IDictionary> GetProviderEventMap() + { + return new Dictionary>() + { + { _providerName, new string[] { "EventCounters" } } + }; + } + + public bool HasSatisfiedCondition(TraceEvent traceEvent) + { + // Filter to the counter of interest before forwarding to the implementation + if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload payload)) + { + return _impl.HasSatisfiedCondition(payload); + } + return false; + } + + public static MonitoringSourceConfiguration CreateConfiguration(EventCounterTriggerSettings settings) + { + Validate(settings); + + return new MetricSourceConfiguration(settings.CounterIntervalSeconds, new string[] { settings.ProviderName }); + } + + private static void Validate(EventCounterTriggerSettings settings) + { + ValidationContext context = new(settings); + Validator.ValidateObject(settings, context, validateAllProperties: true); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerFactory.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerFactory.cs new file mode 100644 index 0000000000..f1ccb33b7b --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerFactory.cs @@ -0,0 +1,21 @@ +// 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; +using System.Runtime; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter +{ + /// + /// The trigger factory for the . + /// + internal sealed class EventCounterTriggerFactory : + ITraceEventTriggerFactory + { + public ITraceEventTrigger Create(EventCounterTriggerSettings settings) + { + return new EventCounterTrigger(settings); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs new file mode 100644 index 0000000000..5c2d9c0a23 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs @@ -0,0 +1,92 @@ +// 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; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter +{ + // The core implementation of the EventCounter trigger that processes + // the trigger settings and evaluates the counter payload. Primary motivation + // for the implementation is for unit testability separate from TraceEvent. + internal sealed class EventCounterTriggerImpl + { + private readonly long _intervalTicks; + private readonly Func _valueFilter; + private readonly long _windowTicks; + + private long? _latestTicks; + private long? _targetTicks; + + public EventCounterTriggerImpl(EventCounterTriggerSettings settings) + { + if (null == settings) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (settings.GreaterThan.HasValue) + { + double minValue = settings.GreaterThan.Value; + if (settings.LessThan.HasValue) + { + double maxValue = settings.LessThan.Value; + _valueFilter = value => value > minValue && value < maxValue; + } + else + { + _valueFilter = value => value > minValue; + } + } + else if (settings.LessThan.HasValue) + { + double maxValue = settings.LessThan.Value; + _valueFilter = value => value < maxValue; + } + + _intervalTicks = settings.CounterIntervalSeconds * TimeSpan.TicksPerSecond; + _windowTicks = settings.SlidingWindowDuration.Ticks; + } + + public bool HasSatisfiedCondition(ICounterPayload payload) + { + long payloadTimestampTicks = payload.Timestamp.Ticks; + long payloadIntervalTicks = (long)(payload.Interval * TimeSpan.TicksPerSecond); + + if (!_valueFilter(payload.Value)) + { + // Series was broken; reset state. + _latestTicks = null; + _targetTicks = null; + return false; + } + else if (!_targetTicks.HasValue) + { + // This is the first event in the series. Record latest and target times. + _latestTicks = payloadTimestampTicks; + // The target time should be the start of the first passing interval + the requisite time window. + // The start of the first passing interval is the payload time stamp - the interval time. + _targetTicks = payloadTimestampTicks - payloadIntervalTicks + _windowTicks; + } + else if (_latestTicks.Value + (1.5 * _intervalTicks) < payloadTimestampTicks) + { + // Detected that an event was skipped/dropped because the time between the current + // event and the previous is more that 150% of the requested interval; consecutive + // counter events should not have that large of an interval. Reset for current + // event to be first event in series. Record latest and target times. + _latestTicks = payloadTimestampTicks; + // The target time should be the start of the first passing interval + the requisite time window. + // The start of the first passing interval is the payload time stamp - the interval time. + _targetTicks = payloadTimestampTicks - payloadIntervalTicks + _windowTicks; + } + else + { + // Update latest time to the current event time. + _latestTicks = payloadTimestampTicks; + } + + // Trigger is satisfied when the latest time is larger than the targe time. + return _latestTicks >= _targetTicks; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs new file mode 100644 index 0000000000..8936a0d1c6 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs @@ -0,0 +1,95 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter +{ + /// + /// The settings for the . + /// + internal sealed class EventCounterTriggerSettings : + IValidatableObject + { + internal const int CounterIntervalSeconds_MaxValue = 24 * 60 * 60; // 1 day + internal const int CounterIntervalSeconds_MinValue = 1; // 1 second + + internal const string EitherGreaterThanLessThanMessage = $"Either the {nameof(GreaterThan)} field or the {nameof(LessThan)} field are required."; + + internal const string GreaterThanMustBeLessThanLessThanMessage = $"The {nameof(GreaterThan)} field must be less than the {nameof(LessThan)} field."; + + internal const string SlidingWindowDuration_MaxValue = "1.00:00:00"; // 1 day + internal const string SlidingWindowDuration_MinValue = "00:00:01"; // 1 second + + /// + /// The name of the event provider from which counters will be monitored. + /// + [Required] + public string ProviderName { get; set; } + + /// + /// The name of the event counter from the event provider to monitor. + /// + [Required] + public string CounterName { get; set; } + + /// + /// The lower bound threshold that the event counter value must hold for + /// the duration specified in . + /// + public double? GreaterThan { get; set; } + + /// + /// The upper bound threshold that the event counter value must hold for + /// the duration specified in . + /// + public double? LessThan { get; set; } + + /// + /// The sliding duration of time in which the event counter must maintain a value + /// above, below, or between the thresholds specified by and . + /// + [Range(typeof(TimeSpan), SlidingWindowDuration_MinValue, SlidingWindowDuration_MaxValue)] + public TimeSpan SlidingWindowDuration { get; set; } + + /// + /// The sampling interval of the event counter. + /// + [Range(CounterIntervalSeconds_MinValue, CounterIntervalSeconds_MaxValue)] + public int CounterIntervalSeconds { get; set; } + + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + List results = new(); + + if (!GreaterThan.HasValue && !LessThan.HasValue) + { + results.Add(new ValidationResult( + EitherGreaterThanLessThanMessage, + new[] + { + nameof(GreaterThan), + nameof(LessThan) + })); + } + else if (GreaterThan.HasValue && LessThan.HasValue) + { + if (GreaterThan.Value > LessThan.Value) + { + results.Add(new ValidationResult( + GreaterThanMustBeLessThanLessThanMessage, + new[] + { + nameof(GreaterThan), + nameof(LessThan) + })); + } + } + + return results; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs new file mode 100644 index 0000000000..448145e595 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs @@ -0,0 +1,31 @@ +// 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 Microsoft.Diagnostics.Tracing; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers +{ + /// + /// Interface for all -based triggers. + /// + internal interface ITraceEventTrigger + { + /// + /// Mapping of event providers to event names in which the trigger has an interest. + /// + /// + /// The method may return null to signify that all events can be forwarded to the trigger. + /// Each event provider entry also may have a null or empty list of event names to + /// signify that all events from the provider can be forwarded to the trigger. + /// + IDictionary> GetProviderEventMap(); + + /// + /// Check if the given satisfies the condition + /// described by the trigger. + /// + bool HasSatisfiedCondition(TraceEvent traceEvent); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTriggerFactory.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTriggerFactory.cs new file mode 100644 index 0000000000..51ab73a92a --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTriggerFactory.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers +{ + /// + /// Interface for creating a new instance of the associated + /// trigger from the specified settings. + /// + internal interface ITraceEventTriggerFactory + { + /// + /// Creates a new instance of the associated trigger from the . + /// + ITraceEventTrigger Create(TSettings settings); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs new file mode 100644 index 0000000000..a5b593d20c --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs @@ -0,0 +1,94 @@ +// 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 Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines +{ + /// + /// Starts an event pipe session using the specified configuration and + /// + /// The settings type of the trace event trigger. + internal sealed class EventPipeTriggerPipeline : + EventSourcePipeline> + { + // The callback as provided to the pipeline. Invoked when the trigger condition is satisfied. + // The trigger condition may be satisfied more than once (thus invoking the callback more than + // once) over the lifetime of the pipeline, depending on the implementation of the trigger. + private readonly Action _callback; + + /// + /// The pipeline used to monitor the trace event source from the event pipe using the trigger + /// specified in the settings of the current pipeline. + /// + private TraceEventTriggerPipeline _pipeline; + + // The trigger implementation used to detect a condition in the trace event source. + private ITraceEventTrigger _trigger; + + public EventPipeTriggerPipeline(DiagnosticsClient client, EventPipeTriggerPipelineSettings settings, Action callback) : + base(client, settings) + { + if (null == Settings.Configuration) + { + throw new ArgumentException(FormattableString.Invariant($"The {nameof(settings.Configuration)} property on the settings must not be null."), nameof(settings)); + } + + if (null == Settings.TriggerFactory) + { + throw new ArgumentException(FormattableString.Invariant($"The {nameof(settings.TriggerFactory)} property on the settings must not be null."), nameof(settings)); + } + + _callback = callback; + } + + protected override MonitoringSourceConfiguration CreateConfiguration() + { + return Settings.Configuration; + } + + protected override async Task OnEventSourceAvailable(EventPipeEventSource eventSource, Func stopSessionAsync, CancellationToken token) + { + _trigger = Settings.TriggerFactory.Create(Settings.TriggerSettings); + + _pipeline = new TraceEventTriggerPipeline(eventSource, _trigger, _callback); + + await _pipeline.RunAsync(token).ConfigureAwait(false); + } + + protected override async Task OnStop(CancellationToken token) + { + if (null != _pipeline) + { + await _pipeline.StopAsync(token).ConfigureAwait(false); + } + await base.OnStop(token); + } + + protected override async Task OnCleanup() + { + if (null != _pipeline) + { + await _pipeline.DisposeAsync().ConfigureAwait(false); + } + + // Disposal is not part of the ITraceEventTrigger interface; check the implementation + // of the trigger to see if it implements one of the disposal interfaces and call it. + if (_trigger is IAsyncDisposable asyncDisposableTrigger) + { + await asyncDisposableTrigger.DisposeAsync().ConfigureAwait(false); + } + else if (_trigger is IDisposable disposableTrigger) + { + disposableTrigger.Dispose(); + } + + await base.OnCleanup(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipelineSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipelineSettings.cs new file mode 100644 index 0000000000..c200c2cdc4 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipelineSettings.cs @@ -0,0 +1,26 @@ +// 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. + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines +{ + internal sealed class EventPipeTriggerPipelineSettings : + EventSourcePipelineSettings + { + /// + /// The event pipe configuration used to collect trace event information for the trigger + /// to use to determine if the trigger condition is satisfied. + /// + public MonitoringSourceConfiguration Configuration { get; set; } + + /// + /// The factory that produces the trigger instantiation. + /// + public ITraceEventTriggerFactory TriggerFactory { get; set; } + + /// + /// The settings to pass to the trigger factory when creating the trigger. + /// + public TSettings TriggerSettings { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs new file mode 100644 index 0000000000..6bdf405d34 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs @@ -0,0 +1,111 @@ +// 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 Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines +{ + /// + /// A pipeline that detects a condition (as specified by the trigger) within the event stream + /// of the specified event source. The callback is invoked for each instance of the detected condition. + /// + internal sealed class TraceEventTriggerPipeline : Pipeline + { + // The callback as provided to the pipeline. Invoked when the trigger condition is satisfied. + // The trigger condition may be satisfied more than once (thus invoking the callback more than + // once) over the lifetime of the pipeline, depending on the implementation of the trigger. + private readonly Action _callback; + + // Completion source to help coordinate running and stopping the pipeline. + private readonly TaskCompletionSource _completionSource = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // The source of thr trace events to monitor. + private readonly TraceEventSource _eventSource; + + // The trigger implementation used to detect a condition in the trace event source. + private readonly ITraceEventTrigger _trigger; + + public TraceEventTriggerPipeline(TraceEventSource eventSource, ITraceEventTrigger trigger, Action callback) + { + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + _eventSource = eventSource ?? throw new ArgumentNullException(nameof(eventSource)); + _trigger = trigger ?? throw new ArgumentNullException(nameof(trigger)); + + IDictionary> providerEventMap = _trigger.GetProviderEventMap(); + if (null == providerEventMap) + { + // Allow all events to be forwarded to the trigger + _eventSource.Dynamic.AddCallbackForProviderEvents( + null, + TraceEventCallback); + } + else + { + // Only allow events described in the mapping to be forwarded to the trigger. + // If a provider has no events specified, then all events from that provider are forwarded. + _eventSource.Dynamic.AddCallbackForProviderEvents( + (string providerName, string eventName) => + { + if (!providerEventMap.TryGetValue(providerName, out IEnumerable eventNames)) + { + return EventFilterResponse.RejectProvider; + } + else if (null == eventNames) + { + return EventFilterResponse.AcceptEvent; + } + else if (!eventNames.Contains(eventName, StringComparer.Ordinal)) + { + return EventFilterResponse.RejectEvent; + } + return EventFilterResponse.AcceptEvent; + }, + TraceEventCallback); + } + } + + protected override async Task OnRun(CancellationToken token) + { + using var _ = token.Register(() => _completionSource.TrySetCanceled(token)); + + await _completionSource.Task.ConfigureAwait(false); + } + + protected override Task OnStop(CancellationToken token) + { + _completionSource.TrySetResult(null); + + return base.OnStop(token); + } + + protected override Task OnCleanup() + { + _completionSource.TrySetCanceled(); + + _eventSource.Dynamic.RemoveCallback(TraceEventCallback); + + return base.OnCleanup(); + } + + private void TraceEventCallback(TraceEvent obj) + { + // Check if processing of in-flight events should be ignored + // due to pipeline in the midst of stopping. + if (!_completionSource.Task.IsCompleted) + { + // If the trigger condition has been satified, invoke the callback + if (_trigger.HasSatisfiedCondition(obj)) + { + _callback(obj); + } + } + } + } +} diff --git a/src/tests/EventPipeTracee/Program.cs b/src/tests/EventPipeTracee/Program.cs index d4b469cea9..49ca395d45 100644 --- a/src/tests/EventPipeTracee/Program.cs +++ b/src/tests/EventPipeTracee/Program.cs @@ -16,10 +16,11 @@ class Program static void Main(string[] args) { - TestBody(args[0]); + bool spinWait10 = args.Length > 1 && args[1] == "SpinWait"; + TestBody(args[0], spinWait10); } - private static void TestBody(string loggerCategory) + private static void TestBody(string loggerCategory, bool spinWait) { Console.Error.WriteLine("Starting remote test process"); Console.Error.Flush(); @@ -51,6 +52,21 @@ private static void TestBody(string loggerCategory) //Signal end of test data Console.WriteLine("1"); + if (spinWait) + { + DateTime targetDateTime = DateTime.UtcNow.AddSeconds(10); + + long acc = 0; + while (DateTime.UtcNow < targetDateTime) + { + acc++; + if (acc % 1_000_000 == 0) + { + Console.Error.WriteLine("Spin waiting..."); + } + } + } + Console.Error.WriteLine($"{DateTime.UtcNow} Awaiting end"); Console.Error.Flush(); if (Console.Read() == -1) diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs new file mode 100644 index 0000000000..0c1cce3f85 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests +{ + internal static class EventCounterConstants + { + public const string RuntimeProviderName = "System.Runtime"; + + public const string EventCountersEventName = "EventCounters"; + + public const string CpuUsageCounterName = "cpu-usage"; + public const string CpuUsageDisplayName = "CPU Usage"; + public const string CpuUsageUnits = "%"; + } +} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs new file mode 100644 index 0000000000..9f458b72c7 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs @@ -0,0 +1,489 @@ +// 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 Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter; +using Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.NETCore.Client.UnitTests; +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests +{ + public class EventCounterTriggerTests + { + private readonly ITestOutputHelper _output; + + public EventCounterTriggerTests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Tests the validation of the fields of the trigger settings. + /// + [Fact] + public void EventCounterTriggerSettingsValidationTest() + { + EventCounterTriggerSettings settings = new(); + + // ProviderName is required. + ValidateRequiredFieldValidation( + settings, + nameof(EventCounterTriggerSettings.ProviderName)); + + settings.ProviderName = EventCounterConstants.RuntimeProviderName; + + // CounterName is required. + ValidateRequiredFieldValidation( + settings, + nameof(EventCounterTriggerSettings.CounterName)); + + settings.CounterName = "exception-count"; + + // SlidingWindowDuration must be specified within valid range. + ValidateRangeFieldValidation( + settings, + nameof(EventCounterTriggerSettings.SlidingWindowDuration), + EventCounterTriggerSettings.SlidingWindowDuration_MinValue, + EventCounterTriggerSettings.SlidingWindowDuration_MaxValue); + + settings.SlidingWindowDuration = TimeSpan.FromSeconds(15); + + // CounterIntervalSeconds must be specified within valid range. + ValidateRangeFieldValidation( + settings, + nameof(EventCounterTriggerSettings.CounterIntervalSeconds), + EventCounterTriggerSettings.CounterIntervalSeconds_MinValue.ToString(CultureInfo.InvariantCulture), + EventCounterTriggerSettings.CounterIntervalSeconds_MaxValue.ToString(CultureInfo.InvariantCulture)); + + settings.CounterIntervalSeconds = 2; + + // Either GreaterThan or LessThan must be specified + ValidateFieldValidation( + settings, + EventCounterTriggerSettings.EitherGreaterThanLessThanMessage, + new[] { nameof(EventCounterTriggerSettings.GreaterThan), nameof(EventCounterTriggerSettings.LessThan) }); + + settings.GreaterThan = 10; + + // Settings object should now pass validation + EventCounterTrigger trigger = new(settings); + + // GreaterThan must be less than LessThan + settings.LessThan = 5; + ValidateFieldValidation( + settings, + EventCounterTriggerSettings.GreaterThanMustBeLessThanLessThanMessage, + new[] { nameof(EventCounterTriggerSettings.GreaterThan), nameof(EventCounterTriggerSettings.LessThan) }); + } + + /// + /// Validates that the usage of the settings will result in a ValidationException thrown + /// with the expected error message and member names. + /// + private void ValidateFieldValidation(EventCounterTriggerSettings settings, string expectedMessage, string[] expectedMemberNames) + { + var exception = Assert.Throws(() => new EventCounterTrigger(settings)); + + Assert.NotNull(exception.ValidationResult); + + Assert.Equal(expectedMessage, exception.ValidationResult.ErrorMessage); + + Assert.Equal(expectedMemberNames, exception.ValidationResult.MemberNames); + } + + /// + /// Validates that the given member name will yield a requiredness validation exception when not specified. + /// + private void ValidateRequiredFieldValidation(EventCounterTriggerSettings settings, string memberName) + { + RequiredAttribute requiredAttribute = new(); + ValidateFieldValidation(settings, requiredAttribute.FormatErrorMessage(memberName), new[] { memberName }); + } + + /// + /// Validates that the given member name will yield a range validation exception when not in the valid range. + /// + private void ValidateRangeFieldValidation(EventCounterTriggerSettings settings, string memberName, string min, string max) + { + RangeAttribute rangeAttribute = new(typeof(T), min, max); + ValidateFieldValidation(settings, rangeAttribute.FormatErrorMessage(memberName), new[] { memberName }); + } + + /// + /// Test that the trigger condition can be satisfied when detecting counter + /// values higher than the specified threshold for a duration of time. + /// + [Fact] + public void EventCounterTriggerGreaterThanTest() + { + // The counter value must be greater than 0.70 for at least 3 seconds. + const double Threshold = 70; // 70% + const int Interval = 1; // 1 second + TimeSpan WindowDuration = TimeSpan.FromSeconds(3); + + CpuData[] data = new CpuData[] + { + new(65, false), + new(67, false), + new(71, false), + new(73, false), + new(74, null), // Unknown depending if sum of intervals is larger than window + new(72, true), + new(71, true), + new(70, false), // Value must be greater than threshold + new(68, false), + new(66, false), + new(70, false), + new(71, false), + new(74, false), + new(73, null), // Unknown depending if sum of intervals is larger than window + new(75, true), + new(72, true), + new(73, true), + new(71, true), + new(69, false), + new(67, false) + }; + + EventCounterTriggerSettings settings = new() + { + ProviderName = EventCounterConstants.RuntimeProviderName, + CounterName = EventCounterConstants.CpuUsageCounterName, + GreaterThan = Threshold, + CounterIntervalSeconds = Interval, + SlidingWindowDuration = WindowDuration + }; + + SimulateDataVerifyTrigger(settings, data); + } + + /// + /// Test that the trigger condition can be satisfied when detecting counter + /// values lower than the specified threshold for a duration of time. + /// + [Fact] + public void EventCounterTriggerLessThanTest() + { + // The counter value must be less than 0.70 for at least 3 seconds. + const double Threshold = 70; // 70% + const int Interval = 1; // 1 second + TimeSpan WindowDuration = TimeSpan.FromSeconds(3); + + CpuData[] data = new CpuData[] + { + new(65, false), + new(67, false), + new(66, null), // Unknown depending if sum of intervals is larger than window + new(68, true), + new(69, true), + new(70, false), // Value must be less than threshold + new(71, false), + new(68, false), + new(66, false), + new(68, null), // Unknown depending if sum of intervals is larger than window + new(67, true), + new(65, true), + new(64, true), + new(71, false), + new(73, false) + }; + + EventCounterTriggerSettings settings = new() + { + ProviderName = EventCounterConstants.RuntimeProviderName, + CounterName = EventCounterConstants.CpuUsageCounterName, + LessThan = Threshold, + CounterIntervalSeconds = Interval, + SlidingWindowDuration = WindowDuration + }; + + SimulateDataVerifyTrigger(settings, data); + } + + /// + /// Test that the trigger condition can be satisfied when detecting counter + /// values that fall between two thresholds for a duration of time. + /// + [Fact] + public void EventCounterTriggerRangeTest() + { + // The counter value must be between 0.25 and 0.35 for at least 8 seconds. + const double LowerThreshold = 25; // 25% + const double UpperThreshold = 35; // 35% + const int Interval = 2; // 2 seconds + TimeSpan WindowDuration = TimeSpan.FromSeconds(8); + + CpuData[] data = new CpuData[] + { + new(23, false), + new(25, false), + new(26, false), + new(27, false), + new(28, false), + new(29, null), // Unknown depending if sum of intervals is larger than window + new(30, true), + new(31, true), + new(33, true), + new(35, false), + new(37, false), + new(34, false), + new(33, false), + new(31, false), + new(30, null), // Unknown depending if sum of intervals is larger than window + new(29, true), + new(27, true), + new(26, true), + new(24, false) + }; + + EventCounterTriggerSettings settings = new() + { + ProviderName = EventCounterConstants.RuntimeProviderName, + CounterName = EventCounterConstants.CpuUsageCounterName, + GreaterThan = LowerThreshold, + LessThan = UpperThreshold, + CounterIntervalSeconds = Interval, + SlidingWindowDuration = WindowDuration + }; + + SimulateDataVerifyTrigger(settings, data); + } + + /// + /// Test that the trigger condition will not be satisfied if successive + /// counter events are missing from the stream (e.g. events are dropped due + /// to event pipe buffer being filled). + /// + [Fact] + public void EventCounterTriggerDropTest() + { + // The counter value must be greater than 0.50 for at least 10 seconds. + const double Threshold = 50; // 50% + const int Interval = 2; // 2 second + TimeSpan WindowDuration = TimeSpan.FromSeconds(10); + + CpuData[] data = new CpuData[] + { + new(53, false), + new(54, false), + new(51, false), + new(52, false), + new(54, null), // Unknown depending if sum of intervals is larger than window + new(53, true), + new(52, true, drop: true), + new(51, false), + new(54, false), + new(58, false), + new(53, false), + new(54, null), // Unknown depending if sum of intervals is larger than window + new(51, true), + new(54, true), + new(54, true, drop: true), + new(52, false), + new(57, false), + new(59, false), + new(54, false), + new(53, null), // Unknown depending if sum of intervals is larger than window + new(51, true), + new(53, true), + new(47, false) + }; + + EventCounterTriggerSettings settings = new() + { + ProviderName = EventCounterConstants.RuntimeProviderName, + CounterName = EventCounterConstants.CpuUsageCounterName, + GreaterThan = Threshold, + CounterIntervalSeconds = Interval, + SlidingWindowDuration = WindowDuration + }; + + SimulateDataVerifyTrigger(settings, data); + } + + /// + /// Tests that the trigger condition can be detected on a live application + /// using the EventPipeTriggerPipeline. + /// + [Fact] + public async Task EventCounterTriggerWithEventPipePipelineTest() + { + EventCounterTriggerSettings settings = new() + { + ProviderName = EventCounterConstants.RuntimeProviderName, + CounterName = EventCounterConstants.CpuUsageCounterName, + GreaterThan = 5, + SlidingWindowDuration = TimeSpan.FromSeconds(3), + CounterIntervalSeconds = 1 + }; + + await using (var testExecution = StartTraceeProcess("TriggerRemoteTest")) + { + //TestRunner should account for start delay to make sure that the diagnostic pipe is available. + + DiagnosticsClient client = new(testExecution.TestRunner.Pid); + + TaskCompletionSource waitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + await using EventPipeTriggerPipeline pipeline = new( + client, + new EventPipeTriggerPipelineSettings + { + Configuration = EventCounterTrigger.CreateConfiguration(settings), + TriggerFactory = new EventCounterTriggerFactory(), + TriggerSettings = settings, + Duration = Timeout.InfiniteTimeSpan + }, + traceEvent => + { + waitSource.TrySetResult(null); + }); + + await PipelineTestUtilities.ExecutePipelineWithDebugee( + _output, + pipeline, + testExecution, + waitSource); + + Assert.True(waitSource.Task.IsCompletedSuccessfully); + } + } + + /// + /// Run the specified sample CPU data through a simple simulation to test the capabilities + /// of the event counter trigger. This uses a random number seed to generate random variations + /// in timestamp and interval data. + /// + private void SimulateDataVerifyTrigger(EventCounterTriggerSettings settings, CpuData[] cpuData) + { + Random random = new Random(); + int seed = random.Next(); + _output.WriteLine("Simulation seed: {0}", seed); + SimulateDataVerifyTrigger(settings, cpuData, seed); + } + + /// + /// Run the specified sample CPU data through a simple simulation to test the capabilities + /// of the event counter trigger. This uses the specified seed value to seed the RNG that produces + /// random variations in timestamp and interval data; allows for replayability of generated variations. + /// + private void SimulateDataVerifyTrigger(EventCounterTriggerSettings settings, CpuData[] cpuData, int seed) + { + EventCounterTriggerImpl trigger = new(settings); + + CpuUsagePayloadFactory payloadFactory = new(seed, settings.CounterIntervalSeconds); + + for (int i = 0; i < cpuData.Length; i++) + { + ref CpuData data = ref cpuData[i]; + _output.WriteLine("Data: Value={0}, Expected={1}, Drop={2}", data.Value, data.Result, data.Drop); + ICounterPayload payload = payloadFactory.CreateNext(data.Value); + if (data.Drop) + { + continue; + } + bool actualResult = trigger.HasSatisfiedCondition(payload); + if (data.Result.HasValue) + { + Assert.Equal(data.Result.Value, actualResult); + } + } + } + + private RemoteTestExecution StartTraceeProcess(string loggerCategory) + { + return RemoteTestExecution.StartProcess(CommonHelper.GetTraceePathWithArgs("EventPipeTracee") + " " + loggerCategory + " SpinWait", _output); + } + + private sealed class CpuData + { + public CpuData(double value, bool? result, bool drop = false) + { + Drop = drop; + Result = result; + Value = value; + } + + /// + /// Specifies if the data should be "dropped" to simulate dropping of events. + /// + public bool Drop { get; } + + /// + /// The expected result of evaluating the trigger on this data. + /// + public bool? Result { get;} + + /// + /// The sample CPU value to be given to the trigger for evaluation. + /// + public double Value { get; } + } + + /// + /// Creates CPU Usage payloads in successive order, simulating the data produced + /// for the cpu-usage counter from the runtime. + /// + private sealed class CpuUsagePayloadFactory + { + private readonly int _intervalSeconds; + private readonly Random _random; + + private DateTime? _lastTimestamp; + + public CpuUsagePayloadFactory(int seed, int intervalSeconds) + { + _intervalSeconds = intervalSeconds; + _random = new Random(seed); + } + + /// + /// Creates the next counter payload based on the provided value. + /// + /// + /// The timestamp is roughly incremented by the specified interval from the constructor + /// in order to simulate variations in the timestamp of counter events as seen in real + /// event data. The actual interval is also roughly generated from specified interval + /// from the constructor to simulate variations in the collection interval as seen in + /// real event data. + /// + public ICounterPayload CreateNext(double value) + { + // Add some variance between -5 to 5 milliseconds to simulate "real" interval value. + float actualInterval = Convert.ToSingle(_intervalSeconds + (_random.NextDouble() / 100) - 0.005); + + if (!_lastTimestamp.HasValue) + { + // Start with the current time + _lastTimestamp = DateTime.UtcNow; + } + else + { + // Increment timestamp by one whole interval + _lastTimestamp = _lastTimestamp.Value.AddSeconds(actualInterval); + } + + // Add some variance between -5 and 5 milliseconds to simulate "real" timestamp + _lastTimestamp = _lastTimestamp.Value.AddMilliseconds((10 * _random.NextDouble()) - 5); + + return new CounterPayload( + _lastTimestamp.Value, + EventCounterConstants.RuntimeProviderName, + EventCounterConstants.CpuUsageCounterName, + EventCounterConstants.CpuUsageDisplayName, + EventCounterConstants.CpuUsageUnits, + value, + CounterType.Metric, + actualInterval); + } + } + } +} From 72376f66c0fe2ab02c70af60d7703cfb97c1465e Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Mon, 16 Aug 2021 18:08:24 -0700 Subject: [PATCH 2/6] Remove constant interpolated strings. --- .../Triggers/EventCounter/EventCounterTriggerSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs index 8936a0d1c6..70a1328aed 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs @@ -17,9 +17,9 @@ internal sealed class EventCounterTriggerSettings : internal const int CounterIntervalSeconds_MaxValue = 24 * 60 * 60; // 1 day internal const int CounterIntervalSeconds_MinValue = 1; // 1 second - internal const string EitherGreaterThanLessThanMessage = $"Either the {nameof(GreaterThan)} field or the {nameof(LessThan)} field are required."; + internal const string EitherGreaterThanLessThanMessage = "Either the " + nameof(GreaterThan) + " field or the " + nameof(LessThan) + " field are required."; - internal const string GreaterThanMustBeLessThanLessThanMessage = $"The {nameof(GreaterThan)} field must be less than the {nameof(LessThan)} field."; + internal const string GreaterThanMustBeLessThanLessThanMessage = "The " + nameof(GreaterThan) + " field must be less than the " + nameof(LessThan) + " field."; internal const string SlidingWindowDuration_MaxValue = "1.00:00:00"; // 1 day internal const string SlidingWindowDuration_MinValue = "00:00:01"; // 1 second From 691ec1c3bbb05050e4bd0cbf57c216ec254c28d1 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Tue, 17 Aug 2021 11:45:53 -0700 Subject: [PATCH 3/6] PR Feedback --- .../Triggers/EventCounter/EventCounterTriggerImpl.cs | 2 +- .../Triggers/EventCounter/EventCounterTriggerSettings.cs | 2 +- .../Triggers/Pipelines/TraceEventTriggerPipeline.cs | 2 +- src/tests/EventPipeTracee/Program.cs | 6 +++--- .../EventCounterConstants.cs | 6 +++--- .../EventCounterTriggerTests.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs index 5c2d9c0a23..909e6452c0 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs @@ -85,7 +85,7 @@ public bool HasSatisfiedCondition(ICounterPayload payload) _latestTicks = payloadTimestampTicks; } - // Trigger is satisfied when the latest time is larger than the targe time. + // Trigger is satisfied when the latest time is larger than the target time. return _latestTicks >= _targetTicks; } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs index 70a1328aed..88331ecd5f 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs @@ -77,7 +77,7 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali } else if (GreaterThan.HasValue && LessThan.HasValue) { - if (GreaterThan.Value > LessThan.Value) + if (GreaterThan.Value >= LessThan.Value) { results.Add(new ValidationResult( GreaterThanMustBeLessThanLessThanMessage, diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs index 6bdf405d34..eabde7db7d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs @@ -26,7 +26,7 @@ internal sealed class TraceEventTriggerPipeline : Pipeline private readonly TaskCompletionSource _completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // The source of thr trace events to monitor. + // The source of the trace events to monitor. private readonly TraceEventSource _eventSource; // The trigger implementation used to detect a condition in the trace event source. diff --git a/src/tests/EventPipeTracee/Program.cs b/src/tests/EventPipeTracee/Program.cs index 49ca395d45..0daa16664f 100644 --- a/src/tests/EventPipeTracee/Program.cs +++ b/src/tests/EventPipeTracee/Program.cs @@ -16,11 +16,11 @@ class Program static void Main(string[] args) { - bool spinWait10 = args.Length > 1 && args[1] == "SpinWait"; + bool spinWait10 = args.Length > 1 && "SpinWait10".Equals(args[1], StringComparison.Ordinal); TestBody(args[0], spinWait10); } - private static void TestBody(string loggerCategory, bool spinWait) + private static void TestBody(string loggerCategory, bool spinWait10) { Console.Error.WriteLine("Starting remote test process"); Console.Error.Flush(); @@ -52,7 +52,7 @@ private static void TestBody(string loggerCategory, bool spinWait) //Signal end of test data Console.WriteLine("1"); - if (spinWait) + if (spinWait10) { DateTime targetDateTime = DateTime.UtcNow.AddSeconds(10); diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs index 0c1cce3f85..8e6e51e816 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterConstants.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; +// 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. namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests { diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs index 9f458b72c7..9151f93a7f 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/EventCounterTriggerTests.cs @@ -400,7 +400,7 @@ private void SimulateDataVerifyTrigger(EventCounterTriggerSettings settings, Cpu private RemoteTestExecution StartTraceeProcess(string loggerCategory) { - return RemoteTestExecution.StartProcess(CommonHelper.GetTraceePathWithArgs("EventPipeTracee") + " " + loggerCategory + " SpinWait", _output); + return RemoteTestExecution.StartProcess(CommonHelper.GetTraceePathWithArgs("EventPipeTracee") + " " + loggerCategory + " SpinWait10", _output); } private sealed class CpuData From bdc8fc18a2e62d448e76812fafbb245c5a694b5d Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Thu, 19 Aug 2021 11:48:23 -0700 Subject: [PATCH 4/6] Correctly set comparer settings for providers and event counters. --- .../Triggers/Pipelines/EventPipeTriggerPipeline.cs | 3 ++- .../Triggers/Pipelines/TraceEventTriggerPipeline.cs | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs index a5b593d20c..cec4da990e 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/EventPipeTriggerPipeline.cs @@ -11,7 +11,8 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines { /// - /// Starts an event pipe session using the specified configuration and + /// Starts an event pipe session using the specified configuration and invokes a callback + /// when events from the session satisfy the specified trigger. /// /// The settings type of the trace event trigger. internal sealed class EventPipeTriggerPipeline : diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs index eabde7db7d..458a04620c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs @@ -38,8 +38,8 @@ public TraceEventTriggerPipeline(TraceEventSource eventSource, ITraceEventTrigge _eventSource = eventSource ?? throw new ArgumentNullException(nameof(eventSource)); _trigger = trigger ?? throw new ArgumentNullException(nameof(trigger)); - IDictionary> providerEventMap = _trigger.GetProviderEventMap(); - if (null == providerEventMap) + IDictionary> providerEventMapFromTrigger = _trigger.GetProviderEventMap(); + if (null == providerEventMapFromTrigger) { // Allow all events to be forwarded to the trigger _eventSource.Dynamic.AddCallbackForProviderEvents( @@ -48,6 +48,13 @@ public TraceEventTriggerPipeline(TraceEventSource eventSource, ITraceEventTrigge } else { + // Event providers should be compared case-insensitive whereas counter names should be compared case-sensative. + // Make a copy of the provided map and change the comparers as appropriate. + IDictionary> providerEventMap = providerEventMapFromTrigger.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.ToArray().AsEnumerable(), + StringComparer.OrdinalIgnoreCase); + // Only allow events described in the mapping to be forwarded to the trigger. // If a provider has no events specified, then all events from that provider are forwarded. _eventSource.Dynamic.AddCallbackForProviderEvents( From 8361787bc5101139a16528c6c2c6ad4e9996c32e Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Tue, 24 Aug 2021 20:58:33 -0700 Subject: [PATCH 5/6] PR Feedback --- .../Triggers/EventCounter/EventCounterTrigger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs index 9ba7c0104c..43de0f9883 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs @@ -32,7 +32,7 @@ public EventCounterTrigger(EventCounterTriggerSettings settings) _filter = new CounterFilter(settings.CounterIntervalSeconds); _filter.AddFilter(settings.ProviderName, new string[] { settings.CounterName }); - _impl = new(settings); + _impl = new EventCounterTriggerImpl(settings); _providerName = settings.ProviderName; } From 106695c37849db2c91fb51c6aca06d47deec36f9 Mon Sep 17 00:00:00 2001 From: Justin Anderson Date: Tue, 24 Aug 2021 21:25:33 -0700 Subject: [PATCH 6/6] Change event map interfaces to be readonly. Cache event map in EventCounterTrigger. --- .../EventCounter/EventCounterTrigger.cs | 30 +++++++++++++++---- .../Triggers/ITraceEventTrigger.cs | 2 +- .../Pipelines/TraceEventTriggerPipeline.cs | 4 ++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs index 43de0f9883..622171d07c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTrigger.cs @@ -4,7 +4,9 @@ using Microsoft.Diagnostics.Tracing; using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter @@ -16,6 +18,18 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter internal sealed class EventCounterTrigger : ITraceEventTrigger { + // A cache of the list of events that are expected from the specified event provider. + // This is a mapping of event provider name to the event map returned by GetProviderEventMap. + // This allows caching of the event map between multiple instances of the trigger that + // use the same event provider as the source of counter events. + private static readonly ConcurrentDictionary>> _eventMapCache = + new ConcurrentDictionary>>(StringComparer.OrdinalIgnoreCase); + + // Only care for the EventCounters events from any of the specified providers, thus + // create a static readonly instance that is shared among all event maps. + private static readonly IReadOnlyCollection _eventProviderEvents = + new ReadOnlyCollection(new string[] { "EventCounters" }); + private readonly CounterFilter _filter; private readonly EventCounterTriggerImpl _impl; private readonly string _providerName; @@ -37,12 +51,9 @@ public EventCounterTrigger(EventCounterTriggerSettings settings) _providerName = settings.ProviderName; } - public IDictionary> GetProviderEventMap() + public IReadOnlyDictionary> GetProviderEventMap() { - return new Dictionary>() - { - { _providerName, new string[] { "EventCounters" } } - }; + return _eventMapCache.GetOrAdd(_providerName, CreateEventMapForProvider); } public bool HasSatisfiedCondition(TraceEvent traceEvent) @@ -67,5 +78,14 @@ private static void Validate(EventCounterTriggerSettings settings) ValidationContext context = new(settings); Validator.ValidateObject(settings, context, validateAllProperties: true); } + + private IReadOnlyDictionary> CreateEventMapForProvider(string providerName) + { + return new ReadOnlyDictionary>( + new Dictionary>() + { + { _providerName, _eventProviderEvents } + }); + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs index 448145e595..3222767250 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/ITraceEventTrigger.cs @@ -20,7 +20,7 @@ internal interface ITraceEventTrigger /// Each event provider entry also may have a null or empty list of event names to /// signify that all events from the provider can be forwarded to the trigger. /// - IDictionary> GetProviderEventMap(); + IReadOnlyDictionary> GetProviderEventMap(); /// /// Check if the given satisfies the condition diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs index 458a04620c..c9d53108df 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs @@ -38,7 +38,9 @@ public TraceEventTriggerPipeline(TraceEventSource eventSource, ITraceEventTrigge _eventSource = eventSource ?? throw new ArgumentNullException(nameof(eventSource)); _trigger = trigger ?? throw new ArgumentNullException(nameof(trigger)); - IDictionary> providerEventMapFromTrigger = _trigger.GetProviderEventMap(); + IReadOnlyDictionary> providerEventMapFromTrigger = + _trigger.GetProviderEventMap(); + if (null == providerEventMapFromTrigger) { // Allow all events to be forwarded to the trigger