}
}
- 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;
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);
}
// 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
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));
}
}
}
// 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;
/// 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);
}
}
}
--- /dev/null
+// 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;
+ }
+ }
+}
--- /dev/null
+// 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
+ }
+}
--- /dev/null
+// 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;
+ }
+ }
+}
--- /dev/null
+// 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>()) }
+ });
+ }
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+}