[Dotnet Monitor] Adding `SystemDiagnosticsMetrics` Trigger (#3697)
authorkkeirstead <85592574+kkeirstead@users.noreply.github.com>
Wed, 8 Mar 2023 22:03:30 +0000 (14:03 -0800)
committerGitHub <noreply@github.com>
Wed, 8 Mar 2023 22:03:30 +0000 (14:03 -0800)
src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MetricSourceConfiguration.cs
src/Microsoft.Diagnostics.Monitoring.EventPipe/Counters/CounterUtilities.cs
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerImpl.cs
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/EventCounter/EventCounterTriggerSettings.cs
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerImplHelper.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerSettingsConstants.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerSettingsValidation.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerFactory.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerSettings.cs [new file with mode: 0644]

index a045a343ec8cc274f6e0177094a7847a1f6f344b..67b241f6f2b3705de8abed3b34157b2a063e72b7 100644 (file)
@@ -85,10 +85,10 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe
             }
         }
 
-        private static IEnumerable<MetricEventPipeProvider> CreateProviders(IEnumerable<string> providers) =>
+        internal static IEnumerable<MetricEventPipeProvider> CreateProviders(IEnumerable<string> providers, MetricType metricType = MetricType.EventCounter) =>
             providers.Select(provider => new MetricEventPipeProvider {
                 Provider = provider,
-                Type = MetricType.EventCounter
+                Type = metricType
             });
 
         public override IList<EventPipeProvider> GetProviders() => _eventPipeProviders;
index 26ead388826cbfd0837894a94c4f56c4f9dbc366..51f4603181751c1f0324a04847bd3fdacf376bb2 100644 (file)
@@ -47,7 +47,9 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe
             return metadataDict;
         }
 
-        public static string AppendPercentile(string tags, double quantile) => AppendPercentile(tags, FormattableString.Invariant($"Percentile={(int)(100 * quantile)}"));
+        public static int CreatePercentile(double quantile) => (int)(100 * quantile);
+
+        public static string AppendPercentile(string tags, double quantile) => AppendPercentile(tags, FormattableString.Invariant($"Percentile={CreatePercentile(quantile)}"));
 
         private static string AppendPercentile(string tags, string percentile) => string.IsNullOrEmpty(tags) ? percentile : string.Concat(tags, ",", percentile);
     }
index 719fb2ae3bee5cfd35ad4f1cd14a11950e2c3840..9f4d40e9e9df9bdf3edfd9b6c05c2f1a867dad6d 100644 (file)
@@ -2,6 +2,7 @@
 // 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.Shared;
 using System;
 
 namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter
@@ -25,68 +26,13 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter
                 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 = (long)(settings.CounterIntervalSeconds * TimeSpan.TicksPerSecond);
-            _windowTicks = settings.SlidingWindowDuration.Ticks;
+            SharedTriggerImplHelper.SetDefaultValueFilter(ref _valueFilter, settings.GreaterThan, settings.LessThan);
+            SharedTriggerImplHelper.SetIntervalAndWindowTicks(ref _intervalTicks, ref _windowTicks, settings.CounterIntervalSeconds, 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 target time.
-            return _latestTicks >= _targetTicks;
+            return SharedTriggerImplHelper.HasSatisfiedCondition(ref _latestTicks, ref _targetTicks, _windowTicks, _intervalTicks, payload, _valueFilter(payload.Value));
         }
     }
 }
index 1fa46cad3085e36b67c6d5699e0bcda7780ac30c..cf6f3492726bcdde450389a64eecc91e62eef407 100644 (file)
@@ -2,6 +2,7 @@
 // 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.Shared;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
@@ -52,44 +53,18 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter
         /// The sliding duration of time in which the event counter must maintain a value
         /// above, below, or between the thresholds specified by <see cref="GreaterThan"/> and <see cref="LessThan"/>.
         /// </summary>
-        [Range(typeof(TimeSpan), SlidingWindowDuration_MinValue, SlidingWindowDuration_MaxValue)]
+        [Range(typeof(TimeSpan), SharedTriggerSettingsConstants.SlidingWindowDuration_MinValue, SharedTriggerSettingsConstants.SlidingWindowDuration_MaxValue)]
         public TimeSpan SlidingWindowDuration { get; set; }
 
         /// <summary>
         /// The sampling interval of the event counter.
         /// </summary>
-        [Range(CounterIntervalSeconds_MinValue, CounterIntervalSeconds_MaxValue)]
+        [Range(SharedTriggerSettingsConstants.CounterIntervalSeconds_MinValue, SharedTriggerSettingsConstants.CounterIntervalSeconds_MaxValue)]
         public float CounterIntervalSeconds { get; set; }
 
         IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
         {
-            List<ValidationResult> 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;
+            return SharedTriggerSettingsValidation.Validate(GreaterThan, LessThan);
         }
     }
 }
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerImplHelper.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerImplHelper.cs
new file mode 100644 (file)
index 0000000..1295dad
--- /dev/null
@@ -0,0 +1,80 @@
+// 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.Shared
+{
+    internal static class SharedTriggerImplHelper
+    {
+        public static bool HasSatisfiedCondition(ref long? latestTicks, ref long? targetTicks, long windowTicks, long intervalTicks, ICounterPayload payload, bool passesValueFilter)
+        {
+            long payloadTimestampTicks = payload.Timestamp.Ticks;
+            long payloadIntervalTicks = (long)(payload.Interval * TimeSpan.TicksPerSecond);
+
+            if (!passesValueFilter)
+            {
+                // 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 target time.
+            return latestTicks >= targetTicks;
+        }
+
+        public static void SetDefaultValueFilter(ref Func<double, bool> valueFilter, double? greaterThan, double? lessThan)
+        {
+            if (greaterThan.HasValue)
+            {
+                double minValue = greaterThan.Value;
+                if (lessThan.HasValue)
+                {
+                    double maxValue = lessThan.Value;
+                    valueFilter = value => value > minValue && value < maxValue;
+                }
+                else
+                {
+                    valueFilter = value => value > minValue;
+                }
+            }
+            else if (lessThan.HasValue)
+            {
+                double maxValue = lessThan.Value;
+                valueFilter = value => value < maxValue;
+            }
+        }
+
+        public static void SetIntervalAndWindowTicks(ref long intervalTicks, ref long windowTicks, float counterIntervalSeconds, long slidingWindowDurationTicks)
+        {
+            intervalTicks = (long)(counterIntervalSeconds * TimeSpan.TicksPerSecond);
+            windowTicks = slidingWindowDurationTicks;
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerSettingsConstants.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerSettingsConstants.cs
new file mode 100644 (file)
index 0000000..7c01745
--- /dev/null
@@ -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.
+
+using Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter;
+using Microsoft.Diagnostics.Tracing;
+using System.Collections.Generic;
+
+namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Shared
+{
+    internal class SharedTriggerSettingsConstants
+    {
+        internal const float CounterIntervalSeconds_MaxValue = 24 * 60 * 60; // 1 day
+        internal const float CounterIntervalSeconds_MinValue = 1; // 1 second
+
+        internal const int Percentage_MaxValue = 100;
+        internal const int Percentage_MinValue = 0;
+
+        internal const string EitherGreaterThanLessThanMessage = "Either the " + nameof(EventCounterTriggerSettings.GreaterThan) + " field or the " + nameof(EventCounterTriggerSettings.LessThan) + " field are required.";
+
+        internal const string GreaterThanMustBeLessThanLessThanMessage = "The " + nameof(EventCounterTriggerSettings.GreaterThan) + " field must be less than the " + nameof(EventCounterTriggerSettings.LessThan) + " field.";
+
+        internal const string SlidingWindowDuration_MaxValue = "1.00:00:00"; // 1 day
+        internal const string SlidingWindowDuration_MinValue = "00:00:01"; // 1 second
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerSettingsValidation.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Shared/SharedTriggerSettingsValidation.cs
new file mode 100644 (file)
index 0000000..37eaec5
--- /dev/null
@@ -0,0 +1,45 @@
+// 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;
+using System.Linq;
+
+namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Shared
+{
+    internal static class SharedTriggerSettingsValidation
+    {
+        public static IEnumerable<ValidationResult> Validate(double? GreaterThan, double? LessThan)
+        {
+            List<ValidationResult> results = new();
+
+            if (!GreaterThan.HasValue && !LessThan.HasValue)
+            {
+                results.Add(new ValidationResult(
+                    SharedTriggerSettingsConstants.EitherGreaterThanLessThanMessage,
+                    new[]
+                    {
+                    nameof(GreaterThan),
+                    nameof(LessThan)
+                    }));
+            }
+            else if (GreaterThan.HasValue && LessThan.HasValue)
+            {
+                if (GreaterThan.Value >= LessThan.Value)
+                {
+                    results.Add(new ValidationResult(
+                        SharedTriggerSettingsConstants.GreaterThanMustBeLessThanLessThanMessage,
+                        new[]
+                        {
+                        nameof(GreaterThan),
+                        nameof(LessThan)
+                        }));
+                }
+            }
+
+            return results;
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTrigger.cs
new file mode 100644 (file)
index 0000000..7dc9799
--- /dev/null
@@ -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 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.SystemDiagnosticsMetrics
+{
+    /// <summary>
+    /// Trigger that detects when the specified instrument's value is held
+    /// above, below, or between threshold values for a specified duration of time.
+    /// </summary>
+    internal sealed class SystemDiagnosticsMetricsTrigger :
+        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<string, IReadOnlyDictionary<string, IReadOnlyCollection<string>>> _eventMapCache =
+            new ConcurrentDictionary<string, IReadOnlyDictionary<string, IReadOnlyCollection<string>>>(StringComparer.OrdinalIgnoreCase);
+
+        private readonly CounterFilter _filter;
+        private readonly SystemDiagnosticsMetricsTriggerImpl _impl;
+        private readonly string _meterName;
+        private string _sessionId;
+
+        public SystemDiagnosticsMetricsTrigger(SystemDiagnosticsMetricsTriggerSettings settings)
+        {
+            if (null == settings)
+            {
+                throw new ArgumentNullException(nameof(settings));
+            }
+
+            Validate(settings);
+
+            _filter = new CounterFilter(settings.CounterIntervalSeconds);
+            _filter.AddFilter(settings.MeterName, new string[] { settings.InstrumentName });
+
+            _impl = new SystemDiagnosticsMetricsTriggerImpl(settings);
+
+            _meterName = settings.MeterName;
+
+            _sessionId = settings.SessionId;
+        }
+
+        public IReadOnlyDictionary<string, IReadOnlyCollection<string>> GetProviderEventMap()
+        {
+            return _eventMapCache.GetOrAdd(_meterName, CreateEventMapForProvider);
+        }
+
+        public bool HasSatisfiedCondition(TraceEvent traceEvent)
+        {
+            // Filter to the counter of interest before forwarding to the implementation
+            if (traceEvent.TryGetCounterPayload(_filter, _sessionId, out ICounterPayload payload))
+            {
+                return _impl.HasSatisfiedCondition(payload);
+            }
+            return false;
+        }
+
+        public static MetricSourceConfiguration CreateConfiguration(SystemDiagnosticsMetricsTriggerSettings settings)
+        {
+            Validate(settings);
+
+            var config = new MetricSourceConfiguration(settings.CounterIntervalSeconds, MetricSourceConfiguration.CreateProviders(new string[] { settings.MeterName }, MetricType.Meter), settings.MaxHistograms, settings.MaxTimeSeries);
+            settings.SessionId = config.SessionId;
+
+            return config;
+        }
+
+        private static void Validate(SystemDiagnosticsMetricsTriggerSettings settings)
+        {
+            ValidationContext context = new(settings);
+            Validator.ValidateObject(settings, context, validateAllProperties: true);
+        }
+
+        private IReadOnlyDictionary<string, IReadOnlyCollection<string>> CreateEventMapForProvider(string providerName)
+        {
+            return new ReadOnlyDictionary<string, IReadOnlyCollection<string>>(
+                new Dictionary<string, IReadOnlyCollection<string>>()
+                {
+                    { "System.Diagnostics.Metrics", new ReadOnlyCollection<string>(Array.Empty<string>()) }
+                });
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerFactory.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerFactory.cs
new file mode 100644 (file)
index 0000000..1b0b152
--- /dev/null
@@ -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.SystemDiagnosticsMetrics
+{
+    /// <summary>
+    /// The trigger factory for the <see cref="SystemDiagnosticsMetricsTrigger"/>.
+    /// </summary>
+    internal sealed class SystemDiagnosticsMetricsTriggerFactory :
+        ITraceEventTriggerFactory<SystemDiagnosticsMetricsTriggerSettings>
+    {
+        public ITraceEventTrigger Create(SystemDiagnosticsMetricsTriggerSettings settings)
+        {
+            return new SystemDiagnosticsMetricsTrigger(settings);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerImpl.cs
new file mode 100644 (file)
index 0000000..88b282e
--- /dev/null
@@ -0,0 +1,80 @@
+// 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.Shared;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsMetrics
+{
+    // The core implementation of the SystemDiagnosticsMetrics 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 SystemDiagnosticsMetricsTriggerImpl
+    {
+        private readonly long _intervalTicks;
+        private readonly Func<double, bool> _valueFilterDefault;
+        private readonly Func<Dictionary<int, double>, bool> _valueFilterHistogram;
+        private readonly long _windowTicks;
+
+        private long? _latestTicks;
+        private long? _targetTicks;
+
+        public SystemDiagnosticsMetricsTriggerImpl(SystemDiagnosticsMetricsTriggerSettings settings)
+        {
+            if (null == settings)
+            {
+                throw new ArgumentNullException(nameof(settings));
+            }
+
+            if (settings.HistogramPercentile.HasValue)
+            {
+                Func<double, bool> evalFunc = null;
+                SharedTriggerImplHelper.SetDefaultValueFilter(ref evalFunc, settings.GreaterThan, settings.LessThan);
+
+                _valueFilterHistogram = histogramValues =>
+                {
+                    if (!histogramValues.TryGetValue(settings.HistogramPercentile.Value, out var value) || !evalFunc(value))
+                    {
+                        return false;
+                    }
+
+                    return true;
+                };
+            }
+            else
+            {
+                SharedTriggerImplHelper.SetDefaultValueFilter(ref _valueFilterDefault, settings.GreaterThan, settings.LessThan);
+            }
+
+            SharedTriggerImplHelper.SetIntervalAndWindowTicks(ref _intervalTicks, ref _windowTicks, settings.CounterIntervalSeconds, settings.SlidingWindowDuration.Ticks);
+        }
+
+        public bool HasSatisfiedCondition(ICounterPayload payload)
+        {
+            EventType eventType = payload.EventType;
+
+            if (eventType == EventType.Error || eventType == EventType.CounterEnded)
+            {
+                // not currently logging the error messages
+
+                return false;
+            }
+            else
+            {
+                bool passesValueFilter = (payload is PercentilePayload percentilePayload) ?
+                    _valueFilterHistogram(CreatePayloadDictionary(percentilePayload)) :
+                    _valueFilterDefault(payload.Value);
+
+                return SharedTriggerImplHelper.HasSatisfiedCondition(ref _latestTicks, ref _targetTicks, _windowTicks, _intervalTicks, payload, passesValueFilter);
+            }
+        }
+
+        private Dictionary<int, double> CreatePayloadDictionary(PercentilePayload percentilePayload)
+        {
+            return percentilePayload.Quantiles.ToDictionary(keySelector: p => CounterUtilities.CreatePercentile(p.Percentage), elementSelector: p => p.Value);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SystemDiagnosticsMetricsTrigger/SystemDiagnosticsMetricsTriggerSettings.cs
new file mode 100644 (file)
index 0000000..73f7e78
--- /dev/null
@@ -0,0 +1,73 @@
+// 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.Shared;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.SystemDiagnosticsMetrics
+{
+    /// <summary>
+    /// The settings for the <see cref="SystemDiagnosticsMetricsTrigger"/>.
+    /// </summary>
+    internal sealed class SystemDiagnosticsMetricsTriggerSettings :
+        IValidatableObject
+    {
+        /// <summary>
+        /// The name of the meter from which counters/gauges/histograms/etc. will be monitored.
+        /// </summary>
+        [Required]
+        public string MeterName { get; set; }
+
+        /// <summary>
+        /// The name of the instrument from the event provider to monitor.
+        /// </summary>
+        [Required]
+        public string InstrumentName { get; set; }
+
+        /// <summary>
+        /// The lower bound threshold that the instrument value must hold for
+        /// the duration specified in <see cref="SlidingWindowDuration"/>.
+        /// </summary>
+        public double? GreaterThan { get; set; }
+
+        /// <summary>
+        /// The upper bound threshold that the instrument value must hold for
+        /// the duration specified in <see cref="SlidingWindowDuration"/>.
+        /// </summary>
+        public double? LessThan { get; set; }
+
+        /// <summary>
+        /// When monitoring a histogram, this dictates which percentile
+        /// to compare against using the value in GreaterThan/LessThan
+        /// </summary>
+        [Range(SharedTriggerSettingsConstants.Percentage_MinValue, SharedTriggerSettingsConstants.Percentage_MaxValue)]
+        public int? HistogramPercentile { get; set; }
+
+        /// <summary>
+        /// The sliding duration of time in which the instrument must maintain a value
+        /// above, below, or between the thresholds specified by <see cref="GreaterThan"/> and <see cref="LessThan"/>.
+        /// </summary>
+        [Range(typeof(TimeSpan), SharedTriggerSettingsConstants.SlidingWindowDuration_MinValue, SharedTriggerSettingsConstants.SlidingWindowDuration_MaxValue)]
+        public TimeSpan SlidingWindowDuration { get; set; }
+
+        /// <summary>
+        /// The sampling interval of the instrument.
+        /// </summary>
+        [Range(SharedTriggerSettingsConstants.CounterIntervalSeconds_MinValue, SharedTriggerSettingsConstants.CounterIntervalSeconds_MaxValue)]
+        public float CounterIntervalSeconds { get; set; }
+
+        public int MaxHistograms { get; set; }
+
+        public int MaxTimeSeries { get; set; }
+
+        public string SessionId { get; set; }
+
+        IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
+        {
+            return SharedTriggerSettingsValidation.Validate(GreaterThan, LessThan);
+        }
+    }
+}