From 8f2da33b4902f2878ef3ab5f6350680000572bdc Mon Sep 17 00:00:00 2001 From: Wiktor Kopec Date: Wed, 15 Sep 2021 16:13:15 -0700 Subject: [PATCH 1/5] Asp.net triggers --- .../AspNetTriggerSourceConfiguration.cs | 72 +++++ .../AspNet/AspNetRequestCountTrigger.cs | 34 +++ .../AspNetRequestCountTriggerSettings.cs | 14 + .../AspNet/AspNetRequestDurationTrigger.cs | 102 +++++++ .../AspNetRequestDurationTriggerSettings.cs | 24 ++ .../AspNet/AspNetRequestStatusTrigger.cs | 53 ++++ .../AspNetRequestStatusTriggerSettings.cs | 42 +++ .../Triggers/AspNet/AspNetTrigger.cs | 142 +++++++++ .../Triggers/AspNet/AspNetTriggerFactories.cs | 25 ++ .../Triggers/AspNet/AspNetTriggerSettings.cs | 53 ++++ .../AspNet/IAspNetTraceEventTrigger.cs | 26 ++ .../Pipelines/TraceEventTriggerPipeline.cs | 3 +- .../Triggers/SlidingWindow.cs | 73 +++++ .../AspNetTriggerUnitTests.cs | 270 ++++++++++++++++++ .../SlidingWindowTests.cs | 56 ++++ 15 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTriggerSettings.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerFactories.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs create mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SlidingWindow.cs create mode 100644 src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs create mode 100644 src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/SlidingWindowTests.cs diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs new file mode 100644 index 0000000000..165e4fd9d7 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs @@ -0,0 +1,72 @@ +// 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.Diagnostics.Tracing; +using Microsoft.Diagnostics.NETCore.Client; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe +{ + public sealed class AspNetTriggerSourceConfiguration : MonitoringSourceConfiguration + { + // In order to handle hung requests, we also capture metrics on a regular interval. + // This acts as a wake up timer, since we cannot rely on Activity1Stop. + private readonly bool _supportHeartbeat; + + public const int DefaultHeartbeatInterval = 10; + + public AspNetTriggerSourceConfiguration(bool supportHeartbeat = false) + { + _supportHeartbeat = supportHeartbeat; + } + + /// + /// Filter string for trigger data. Note that even though some triggers use start OR stop, + /// collecting just one causes unusual behavior in data collection. + /// + /// + /// IMPORTANT! We rely on these transformations to make sure we can access relevant data + /// by index. The order must match the data extracted in the triggers. + /// + private const string DiagnosticFilterString = + "Microsoft.AspNetCore/Microsoft.AspNetCore.Hosting.HttpRequestIn.Start@Activity1Start:-" + + "ActivityId=*Activity.Id" + + ";Request.Path" + + ";ActivityStartTime=*Activity.StartTimeUtc.Ticks" + + "\r\n" + + "Microsoft.AspNetCore/Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop@Activity1Stop:-" + + "ActivityId=*Activity.Id" + + ";Request.Path" + + ";Response.StatusCode" + + ";ActivityDuration=*Activity.Duration.Ticks" + + "\r\n"; + + public override IList GetProviders() + { + var providers = new List() + { + // Diagnostic source events + new EventPipeProvider(DiagnosticSourceEventSource, + keywords: 0x1 | 0x2, + eventLevel: EventLevel.Verbose, + arguments: new Dictionary + { + { "FilterAndPayloadSpecs", DiagnosticFilterString } + }) + }; + + if (!_supportHeartbeat) + { + return providers; + } + else + { + return new AggregateSourceConfiguration( + new AspNetTriggerSourceConfiguration(supportHeartbeat: false), + new MetricSourceConfiguration(DefaultHeartbeatInterval, new[] { MicrosoftAspNetCoreHostingEventSourceName })).GetProviders(); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs new file mode 100644 index 0000000000..ad08138c71 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs @@ -0,0 +1,34 @@ +// 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.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestCountTrigger : AspNetTrigger + { + private SlidingWindow _window; + + public AspNetRequestCountTrigger(AspNetRequestCountTriggerSettings settings) : base(settings) + { + _window = new SlidingWindow(settings.SlidingWindowDuration); + } + + protected override bool ActivityStart(DateTime timestamp, string activityId) + { + _window.AddDataPoint(timestamp); + if (_window.Count >= Settings.RequestCount) + { + //Reset trigger + _window.Clear(); + return true; + } + + return base.ActivityStart(timestamp, activityId); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTriggerSettings.cs new file mode 100644 index 0000000000..b50fc728a2 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTriggerSettings.cs @@ -0,0 +1,14 @@ +// 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.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestCountTriggerSettings : AspNetTriggerSettings + { + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs new file mode 100644 index 0000000000..3ba2e57516 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs @@ -0,0 +1,102 @@ +// 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.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestDurationTrigger : AspNetTrigger + { + private readonly long _durationTicks; + + //This is adjusted due to rounding errors on event counter timestamp math. + private readonly TimeSpan _heartBeatInterval = TimeSpan.FromSeconds(AspNetTriggerSourceConfiguration.DefaultHeartbeatInterval - 1); + private SlidingWindow _window; + private Dictionary _requests = new(); + private DateTime _lastHeartbeatProcessed = DateTime.MinValue; + + public AspNetRequestDurationTrigger(AspNetRequestDurationTriggerSettings settings) : base(settings) + { + _durationTicks = Settings.RequestDuration.Ticks; + _window = new SlidingWindow(settings.SlidingWindowDuration); + } + + protected override bool ActivityStart(DateTime timestamp, string activityId) + { + _requests.Add(activityId, timestamp); + + return base.ActivityStart(timestamp, activityId); + } + + protected override bool Heartbeat(DateTime timestamp) + { + //May may get additional heartbeats based on multiple counters or extra intervals. We only + //process the data periodically. + if (timestamp - _lastHeartbeatProcessed > _heartBeatInterval) + { + _lastHeartbeatProcessed = timestamp; + List requestsToRemove = new(); + bool trigger = false; + + foreach (KeyValuePair request in _requests) + { + if ((timestamp - request.Value) >= Settings.RequestDuration) + { + _window.AddDataPoint(timestamp); + + //We don't want to count the request more than once, since it could still finish later. + //At this point we already deeemed it too slow. We also want to make sure we + //clear the cached requests periodically even if they don't finish. + requestsToRemove.Add(request.Key); + if (_window.Count >= Settings.RequestCount) + { + trigger = true; + break; + } + } + } + + foreach(string requestId in requestsToRemove) + { + _requests.Remove(requestId); + } + + if (trigger) + { + //Reset trigger + _window.Clear(); + return true; + } + } + + return base.Heartbeat(timestamp); + } + + protected override bool ActivityStop(DateTime timestamp, string activityId, long durationTicks, int statusCode) + { + if (!_requests.Remove(activityId)) + { + //This request was already removed by the heartbeat. No need to evaluate duration since we don't want to double count the request. + return false; + } + + if (durationTicks >= _durationTicks) + { + _window.AddDataPoint(timestamp); + } + + if (_window.Count >= Settings.RequestCount) + { + //Reset trigger + _window.Clear(); + return true; + } + + return base.ActivityStop(timestamp, activityId, durationTicks, statusCode); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs new file mode 100644 index 0000000000..cc39a544d3 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs @@ -0,0 +1,24 @@ +// 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; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestDurationTriggerSettings : AspNetTriggerSettings + { + public const string RequestDuration_MaxValue = "01:00:00"; // 1 hour + public const string RequestDuration_MinValue = "00:00:01"; // 1 second + + /// + /// The minimum duration of the request to be considered slow. + /// + [Range(typeof(TimeSpan), RequestDuration_MinValue, RequestDuration_MaxValue)] + public TimeSpan RequestDuration { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs new file mode 100644 index 0000000000..19bc7ed7a7 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs @@ -0,0 +1,53 @@ +// 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.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestStatusTrigger : AspNetTrigger + { + private SlidingWindow _window; + private HashSet _statusCodes = new(); + private List<(int Min, int Max)> _statusCodeRanges = new(); + + public AspNetRequestStatusTrigger(AspNetRequestStatusTriggerSettings settings) : base(settings) + { + _window = new SlidingWindow(settings.SlidingWindowDuration); + + foreach(string codeOrRange in settings.StatusCodes) + { + if (codeOrRange.Length == 3) + { + _statusCodes.Add(int.Parse(codeOrRange)); + continue; + } + string[] range = codeOrRange.Split('-'); + _statusCodeRanges.Add((int.Parse(range[0]), int.Parse(range[1]))); + } + } + + protected override bool ActivityStop(DateTime timestamp, string activityId, long durationTicks, int statusCode) + { + if (_statusCodes.Contains(statusCode) || + _statusCodeRanges.Any(r => statusCode >= r.Min && statusCode <= r.Max)) + { + _window.AddDataPoint(timestamp); + } + + if (_window.Count >= Settings.RequestCount) + { + //Reset trigger + _window.Clear(); + return true; + } + + return base.ActivityStop(timestamp, activityId, durationTicks, statusCode); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs new file mode 100644 index 0000000000..e7d47d21e4 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs @@ -0,0 +1,42 @@ +// 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; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestStatusTriggerSettings : AspNetTriggerSettings + { + private const string StatusRegex = "[1-5][0-9]{2}"; + private static readonly string RangeRegex = FormattableString.Invariant($"^{StatusRegex}(-{StatusRegex})?$"); + + /// + /// Specifies the set of status codes for the trigger. This can be individual codes or ranges. + /// E.g. 200;400-500 + /// + [Required] + public string[] StatusCodes { get; set; } + + public override IEnumerable Validate(ValidationContext validationContext) + { + List results = new List(); + + foreach(string statusCode in StatusCodes) + { + if (!Regex.IsMatch(statusCode, RangeRegex)) + { + results.Add(new ValidationResult($"'{statusCode}' is not in the correct format.", + new[] { nameof(StatusCodes) })); + } + } + + return results.Concat(base.Validate(validationContext)); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs new file mode 100644 index 0000000000..316648b599 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs @@ -0,0 +1,142 @@ +// 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.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + /// + /// Base class for all Asp.net triggers. + /// + internal abstract class AspNetTrigger : IAspNetTraceEventTrigger, ITraceEventTrigger where TSettings : AspNetTriggerSettings + { + private const string Activity1Start = "Activity1/Start"; + private const string Activity1Stop = "Activity1/Stop"; + private static readonly Guid CountersGuid = new Guid("{9ded64a4-414c-5251-dcf7-1e4e20c15e70}"); + + protected AspNetTrigger(TSettings settings) + { + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + Validate(settings); + + IncludePaths = new HashSet(Settings.IncludePaths ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + ExcludePaths = new HashSet(Settings.ExcludePaths ?? Array.Empty(), StringComparer.OrdinalIgnoreCase); + } + + private static void Validate(TSettings settings) + { + ValidationContext context = new(settings); + Validator.ValidateObject(settings, context, validateAllProperties: true); + } + + public IReadOnlyDictionary> GetProviderEventMap() + { + return new Dictionary> + { + {MonitoringSourceConfiguration.DiagnosticSourceEventSource, new[]{ Activity1Start, Activity1Stop } }, + {MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName, new[]{ "EventCounters" } } + }; + } + + public TSettings Settings { get; } + + //CONSIDER The current design is to trigger in aggregate across all url matches. + //If we want per path triggering, we can simply setup more triggers. + //TODO Add support for wildcards or globs for paths + protected HashSet IncludePaths { get; } + + protected HashSet ExcludePaths { get; } + + protected virtual bool ActivityStart(DateTime timestamp, string activityId) => false; + + protected virtual bool ActivityStop(DateTime timestamp, string activityId, long durationTicks, int statusCode) => false; + + protected virtual bool Heartbeat(DateTime timestamp) => false; + + public bool HasSatisfiedCondition(TraceEvent traceEvent) + { + //We deconstruct the TraceEvent data to make it easy to write tests + DateTime timeStamp = traceEvent.TimeStamp; + + if (traceEvent.ProviderGuid == CountersGuid) + { + //Heartbeat only + return ((IAspNetTraceEventTrigger)this).HasSatisfiedCondition(timeStamp, eventType: AspnetTriggerEventType.Heartbeat, activityId: null, path: null, statusCode: null, duration: null); + } + else + { + int? statusCode = 200; + long? duration = 50; + AspnetTriggerEventType eventType = AspnetTriggerEventType.Start; + + System.Collections.IList arguments = (System.Collections.IList)traceEvent.PayloadValue(2); + string activityId = ExtractByIndex(arguments, 0); + string path = ExtractByIndex(arguments, 1); + + if (traceEvent.EventName == Activity1Stop) + { + statusCode = int.Parse(ExtractByIndex(arguments, 2)); + duration = long.Parse(ExtractByIndex(arguments, 3)); + eventType = AspnetTriggerEventType.Stop; + } + + return ((IAspNetTraceEventTrigger)this).HasSatisfiedCondition(timeStamp, eventType, activityId, path, statusCode, duration); + } + } + + bool IAspNetTraceEventTrigger.HasSatisfiedCondition(DateTime timestamp, AspnetTriggerEventType eventType, string activityId, string path, int? statusCode, long? duration) + { + if (eventType == AspnetTriggerEventType.Heartbeat) + { + return Heartbeat(timestamp); + } + + if (!CheckPathFilter(path)) + { + //No need to update counts if the path is excluded. + return false; + } + + if (eventType == AspnetTriggerEventType.Start) + { + return ActivityStart(timestamp, activityId); + } + else if (eventType == AspnetTriggerEventType.Stop) + { + return ActivityStop(timestamp, activityId, duration.Value, statusCode.Value); + } + return false; + } + + private bool CheckPathFilter(string path) + { + if (IncludePaths.Count > 0) + { + return IncludePaths.Contains(path); + } + if (ExcludePaths.Count > 0) + { + return !ExcludePaths.Contains(path); + } + return true; + } + + private static string ExtractByIndex(System.Collections.IList arguments, int index) + { + IEnumerable> values = (IEnumerable>)arguments[index]; + //The data is internally organized as two KeyValuePair entries, + //The first entry is { Key, "KeyValue"} + //The second is { Value, "Value"} + //e.g. + //{{ Key:"StatusCode", Value:"200" }} + return (string)values.Last().Value; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerFactories.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerFactories.cs new file mode 100644 index 0000000000..300938e36d --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerFactories.cs @@ -0,0 +1,25 @@ +// 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.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal sealed class AspNetRequestCountTriggerFactory : ITraceEventTriggerFactory + { + public ITraceEventTrigger Create(AspNetRequestCountTriggerSettings settings) => new AspNetRequestCountTrigger(settings); + } + + internal sealed class AspNetRequestDurationTriggerFactory : ITraceEventTriggerFactory + { + public ITraceEventTrigger Create(AspNetRequestDurationTriggerSettings settings) => new AspNetRequestDurationTrigger(settings); + } + + internal sealed class AspNetRequestStatusTriggerFactory : ITraceEventTriggerFactory + { + public ITraceEventTrigger Create(AspNetRequestStatusTriggerSettings settings) => new AspNetRequestStatusTrigger(settings); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs new file mode 100644 index 0000000000..b2d55fa1eb --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs @@ -0,0 +1,53 @@ +// 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; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal class AspNetTriggerSettings : IValidatableObject + { + public const string SlidingWindowDuration_MaxValue = "01:00:00"; // 1 hour + public const string SlidingWindowDuration_MinValue = "00:00:01"; // 1 second + + /// + /// The sliding duration in which an Asp.net request trigger condition must occur. + /// + [Range(typeof(TimeSpan), SlidingWindowDuration_MinValue, SlidingWindowDuration_MaxValue)] + public TimeSpan SlidingWindowDuration { get; set; } + + /// + /// The amount of requests that must accumulate in the sliding window and meet the trigger condition. + /// Note that requests that do not meet the condition do NOT reset the count. + /// + [Range(1, int.MaxValue)] + public int RequestCount { get; set; } + + /// + /// List of request paths to include in the trigger condition, such as "/" and "/About". + /// + public string[] IncludePaths { get; set; } + + /// + /// List of request paths to exclude in the trigger condition. + /// + public string[] ExcludePaths { get; set; } + + public virtual IEnumerable Validate(ValidationContext validationContext) + { + List results = new(); + + if (IncludePaths?.Any() == true && ExcludePaths?.Any() == true) + { + results.Add(new ValidationResult($"Cannot set both {nameof(IncludePaths)} and {nameof(ExcludePaths)}.")); + } + + return results; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs new file mode 100644 index 0000000000..9c8c7edb79 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.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. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet +{ + internal enum AspnetTriggerEventType + { + Start, + Stop, + Heartbeat + } + + /// + /// This interface is for testing. It allows the trigger to be callable with a TraceEvent, + /// or with a deconstruction of the TraceEvent payload while running tests. + /// + internal interface IAspNetTraceEventTrigger + { + bool HasSatisfiedCondition(DateTime timestamp, AspnetTriggerEventType eventType, string activityId, string path, int? statusCode, long? duration); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs index c9d53108df..a6bd87db6c 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/Pipelines/TraceEventTriggerPipeline.cs @@ -54,7 +54,8 @@ public TraceEventTriggerPipeline(TraceEventSource eventSource, ITraceEventTrigge // 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(), + //Accept null or empty, both indicating that any event will be accepted. + kvp => (kvp.Value == null) ? null : (kvp.Value.Count == 0) ? null : kvp.Value.ToArray().AsEnumerable(), StringComparer.OrdinalIgnoreCase); // Only allow events described in the mapping to be forwarded to the trigger. diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SlidingWindow.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SlidingWindow.cs new file mode 100644 index 0000000000..4ce4fc3549 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/SlidingWindow.cs @@ -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 System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers +{ + internal sealed class SlidingWindow + { + //Any events that occur within this interval are merged. + private readonly TimeSpan _interval = TimeSpan.FromSeconds(1); + private readonly LinkedList<(DateTime Timestamp, int Count)> _timeData = new(); + private readonly TimeSpan _window; + + public SlidingWindow(TimeSpan slidingWindow) + { + _window = slidingWindow; + } + + public int Count { get; private set; } + + public void AddDataPoint(DateTime timestamp) + { + //ASSUMPTION! We are always expecting to get events that are equal or increasing in time. + if (_timeData.Last == null) + { + _timeData.AddLast((timestamp, 1)); + Count++; + return; + } + + (DateTime lastTimestamp, int lastCount) = _timeData.Last.Value; + + Debug.Assert(timestamp >= lastTimestamp, "Unexpected timestamp"); + + //Coalesce close points together + if (timestamp - lastTimestamp < _interval) + { + _timeData.Last.Value = (lastTimestamp, lastCount + 1); + Count++; + //No need for further processing since we can't fall out of the sliding window. + return; + } + + _timeData.AddLast((timestamp, 1)); + Count++; + + while (_timeData.First != null) + { + (DateTime firstTimestamp, int firstCount) = _timeData.First.Value; + if (timestamp - firstTimestamp > _window) + { + _timeData.RemoveFirst(); + Count -= firstCount; + } + else + { + break; + } + } + } + + public void Clear() + { + _timeData.Clear(); + Count = 0; + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs new file mode 100644 index 0000000000..b0c034a260 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs @@ -0,0 +1,270 @@ +// 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.AspNet; +using Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests +{ + public class AspNetTriggerUnitTests + { + private readonly ITestOutputHelper _outputHelper; + + public AspNetTriggerUnitTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Fact] + public void TestAspNetRequestCount() + { + AspNetRequestCountTriggerSettings settings = new() + { + IncludePaths = new[] { "/" }, + RequestCount = 3, + SlidingWindowDuration = TimeSpan.FromMinutes(1) + }; + + AspNetRequestCountTriggerFactory factory = new(); + var trigger = (AspNetRequestCountTrigger)factory.Create(settings); + + PayloadGenerator generator = new(); + + var s1 = generator.CreateEvent(DateTime.UtcNow); + + // These should not trigger anything because they are not included + var s2 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(30), "/notIncluded"); + var s3 = generator.CreateEvent(s2.Timestamp, "/notIncluded"); + var s4 = generator.CreateEvent(s2.Timestamp, "/notIncluded"); + + var s5 = generator.CreateEvent(s2.Timestamp); + + //Pushes the first event out of sliding window + var s6 = generator.CreateEvent(s2.Timestamp + TimeSpan.FromSeconds(40)); + + var s7 = generator.CreateEvent(s6.Timestamp + TimeSpan.FromSeconds(0.5)); + + ValidateTriggers(trigger, s1, s2, s3, s4, s5, s6, s7); + } + + [Fact] + public void TestAspNetRequestCountExclusions() + { + AspNetRequestCountTriggerSettings settings = new() + { + ExcludePaths = new[] {"/"}, + RequestCount = 3, + SlidingWindowDuration = TimeSpan.FromMinutes(1) + }; + + AspNetRequestCountTriggerFactory factory = new(); + var trigger = (AspNetRequestCountTrigger)factory.Create(settings); + + PayloadGenerator generator = new(); + + // These should not trigger anything because they are excluded + var s1 = generator.CreateEvent(DateTime.UtcNow); + var s2 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(1)); + var s3 = generator.CreateEvent(s2.Timestamp); + var s4 = generator.CreateEvent(s2.Timestamp); + + var s5 = generator.CreateEvent(s2.Timestamp, "/notExcluded"); + var s6 = generator.CreateEvent(s2.Timestamp, "/notExcluded"); + var s7 = generator.CreateEvent(s6.Timestamp + TimeSpan.FromSeconds(10), "/notExcluded"); + + ValidateTriggers(trigger, s1, s2, s3, s4, s5, s6, s7); + } + + [Fact] + public void TestAspNetDuration() + { + AspNetRequestDurationTriggerSettings settings = new() + { + RequestCount = 3, + RequestDuration = TimeSpan.FromSeconds(3), + SlidingWindowDuration = TimeSpan.FromMinutes(1), + }; + + AspNetRequestDurationTriggerFactory factory = new(); + var trigger = (AspNetRequestDurationTrigger)factory.Create(settings); + + PayloadGenerator generator = new(); + + var s1 = generator.CreateEvent(DateTime.UtcNow); + var e1 = generator.CreateEvent(s1, TimeSpan.FromSeconds(3.1).Ticks); + + var s2 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(1)); + var e2 = generator.CreateEvent(s2, TimeSpan.FromSeconds(10).Ticks); + + //does not exceed duration + var s3 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(1)); + var e3 = generator.CreateEvent(s3, TimeSpan.FromSeconds(1).Ticks); + + //pushes the sliding window past all prevoius events + var s4 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromMinutes(5)); + var e4 = generator.CreateEvent(s4, TimeSpan.FromSeconds(15).Ticks); + + var s5 = generator.CreateEvent(s4.Timestamp + TimeSpan.FromSeconds(1)); + var e5 = generator.CreateEvent(s5, TimeSpan.FromSeconds(10).Ticks); + + var s6 = generator.CreateEvent(s4.Timestamp + TimeSpan.FromSeconds(1)); + var e6 = generator.CreateEvent(s6, TimeSpan.FromSeconds(20).Ticks); + + //Reflects actual ordering + ValidateTriggers(trigger, s1, s2, s3, e3, e1, e2, s4, s5, s6, e5, e4, e6); + } + + [Fact] + public void TestAspNetDurationHungRequests() + { + AspNetRequestDurationTriggerSettings settings = new() + { + RequestCount = 3, + RequestDuration = TimeSpan.FromSeconds(3), + SlidingWindowDuration = TimeSpan.FromMinutes(1), + }; + + AspNetRequestDurationTriggerFactory factory = new(); + var trigger = (AspNetRequestDurationTrigger)factory.Create(settings); + + PayloadGenerator generator = new(); + + var s1 = generator.CreateEvent(DateTime.UtcNow); + var e1 = generator.CreateEvent(s1, TimeSpan.FromMinutes(1).Ticks); + + var s2 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(1)); + var e2 = generator.CreateEvent(s2, TimeSpan.FromMinutes(2).Ticks); + + var s3 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(2)); + var e3 = generator.CreateEvent(s3, TimeSpan.FromMinutes(3).Ticks); + + var h1 = generator.CreateCounterEvent(s1.Timestamp + TimeSpan.FromSeconds(15)); + + ValidateTriggers(trigger, triggerIndex: 3, s1, s2, s3, h1, e1, e2, e3); + } + + [Fact] + public void TestAspNetStatus() + { + AspNetRequestStatusTriggerSettings settings = new() + { + RequestCount = 3, + StatusCodes = new[] { "520", "521", "400-500" }, + SlidingWindowDuration = TimeSpan.FromMinutes(1), + }; + + AspNetRequestStatusTriggerFactory factory = new(); + var trigger = (AspNetRequestStatusTrigger)factory.Create(settings); + + PayloadGenerator generator = new(); + + var s1 = generator.CreateEvent(DateTime.UtcNow); + var e1 = generator.CreateEvent(s1, TimeSpan.FromSeconds(3.1).Ticks, statusCode: 404); + + var s2 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(1)); + var e2 = generator.CreateEvent(s2, TimeSpan.FromSeconds(10).Ticks, statusCode: 420); + + //does not meet status code + var s3 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromSeconds(1)); + var e3 = generator.CreateEvent(s3, TimeSpan.FromSeconds(1).Ticks); + + //pushes the sliding window past all prevoius events + var s4 = generator.CreateEvent(s1.Timestamp + TimeSpan.FromMinutes(5)); + var e4 = generator.CreateEvent(s4, TimeSpan.FromSeconds(15).Ticks, statusCode: 520); + + var s5 = generator.CreateEvent(s4.Timestamp + TimeSpan.FromSeconds(1)); + var e5 = generator.CreateEvent(s5, TimeSpan.FromSeconds(10).Ticks, statusCode: 521); + + //does not meet status code + var s6 = generator.CreateEvent(s4.Timestamp + TimeSpan.FromSeconds(1)); + var e6 = generator.CreateEvent(s5, TimeSpan.FromSeconds(10).Ticks); + + var s7 = generator.CreateEvent(s4.Timestamp + TimeSpan.FromSeconds(1)); + var e7 = generator.CreateEvent(s7, TimeSpan.FromSeconds(20).Ticks, statusCode: 404); + + //Reflects actual ordering + ValidateTriggers(trigger, s1, s2, s3, e3, e1, e2, s4, s5, s6, s7, e5, e4, e6, e7); + } + + private static void ValidateTriggers(IAspNetTraceEventTrigger requestTrigger, params SimulatedTraceEvent[] events) + { + ValidateTriggers(requestTrigger, events.Length - 1, events); + } + + private static void ValidateTriggers(IAspNetTraceEventTrigger requestTrigger, int triggerIndex, params SimulatedTraceEvent[] events) + { + for (int i = 0; i < events.Length; i++) + { + bool shouldSatisfy = i == triggerIndex; + bool validTriggerResult = (shouldSatisfy == requestTrigger.HasSatisfiedCondition( + events[i].Timestamp, + events[i].EventType, + events[i].ActivityId, + events[i].Path, + events[i].StatusCode, + events[i].Duration)); + + Assert.True(validTriggerResult, $"Failed at index {i}"); + } + } + + private sealed class PayloadGenerator + { + public SimulatedTraceEvent CreateCounterEvent(DateTime timestamp) + { + return new SimulatedTraceEvent { Timestamp = timestamp, EventType = AspnetTriggerEventType.Heartbeat }; + } + + public SimulatedTraceEvent CreateEvent(DateTime timestamp, string path = "/", string activityId = null) + { + return new SimulatedTraceEvent + { + Timestamp = timestamp, + EventType = AspnetTriggerEventType.Start, + ActivityId = activityId ?? Guid.NewGuid().ToString(), + Path = path + }; + } + + public SimulatedTraceEvent CreateEvent(SimulatedTraceEvent previousEvent, long duration, int statusCode = 200, string path = null, string activityId = null) + { + Assert.NotNull(previousEvent); + return new SimulatedTraceEvent + { + ActivityId = activityId ?? previousEvent.ActivityId, + Timestamp = previousEvent.Timestamp + TimeSpan.FromTicks(duration), + Path = path ?? previousEvent.Path, + Duration = duration, + EventType = AspnetTriggerEventType.Stop, + StatusCode = statusCode + }; + } + } + + private sealed class SimulatedTraceEvent + { + public DateTime Timestamp { get; set; } + + public AspnetTriggerEventType EventType { get; set; } + + public string ActivityId { get; set; } + + public string Path { get; set; } + + public int? StatusCode { get; set; } + + public long? Duration { get; set; } + } + + } +} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/SlidingWindowTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/SlidingWindowTests.cs new file mode 100644 index 0000000000..8607c983f5 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/SlidingWindowTests.cs @@ -0,0 +1,56 @@ +// 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; +using Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet; +using Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.Pipelines; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests +{ + public class SlidingWindowTests + { + [Fact] + public void TestSlidingWindow() + { + DateTime start = DateTime.UtcNow; + + SlidingWindow window = new SlidingWindow(TimeSpan.FromSeconds(30)); + + window.AddDataPoint(start); + Assert.Equal(1, window.Count); + + window.AddDataPoint(start); + window.AddDataPoint(start); + + window.AddDataPoint(start + TimeSpan.FromSeconds(10)); + window.AddDataPoint(start + TimeSpan.FromSeconds(15)); + window.AddDataPoint(start + TimeSpan.FromSeconds(20)); + window.AddDataPoint(start + TimeSpan.FromSeconds(20.5)); + window.AddDataPoint(start + TimeSpan.FromSeconds(25)); + + Assert.Equal(8, window.Count); + + window.AddDataPoint(start + TimeSpan.FromSeconds(42)); + Assert.Equal(5, window.Count); + + window.AddDataPoint(start + TimeSpan.FromSeconds(52)); + Assert.Equal(3, window.Count); + + window.AddDataPoint(start + TimeSpan.FromSeconds(100)); + Assert.Equal(1, window.Count); + + window.Clear(); + Assert.Equal(0, window.Count); + } + } +} From 86e42202a1c2229f7fa66299b0ba6588e0ad932c Mon Sep 17 00:00:00 2001 From: Wiktor Kopec Date: Thu, 16 Sep 2021 15:25:50 -0700 Subject: [PATCH 2/5] PR Feedback --- .../AspNetTriggerSourceConfiguration.cs | 26 ++++++------ .../AspNet/AspNetRequestCountTrigger.cs | 4 +- .../AspNet/AspNetRequestDurationTrigger.cs | 12 ++---- .../AspNet/AspNetRequestStatusTrigger.cs | 4 +- .../Triggers/AspNet/AspNetTrigger.cs | 40 ++++++++++++------- .../Triggers/AspNet/AspNetTriggerSettings.cs | 6 +-- .../AspNet/IAspNetTraceEventTrigger.cs | 26 ------------ .../AspNetTriggerUnitTests.cs | 4 +- 8 files changed, 49 insertions(+), 73 deletions(-) delete mode 100644 src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs index 165e4fd9d7..186dbc2007 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs @@ -45,27 +45,25 @@ public AspNetTriggerSourceConfiguration(bool supportHeartbeat = false) public override IList GetProviders() { - var providers = new List() + if (_supportHeartbeat) { - // Diagnostic source events - new EventPipeProvider(DiagnosticSourceEventSource, + return new AggregateSourceConfiguration( + new AspNetTriggerSourceConfiguration(supportHeartbeat: false), + new MetricSourceConfiguration(DefaultHeartbeatInterval, new[] { MicrosoftAspNetCoreHostingEventSourceName })).GetProviders(); + + } + else + { + return new[] + { + new EventPipeProvider(DiagnosticSourceEventSource, keywords: 0x1 | 0x2, eventLevel: EventLevel.Verbose, arguments: new Dictionary { { "FilterAndPayloadSpecs", DiagnosticFilterString } }) - }; - - if (!_supportHeartbeat) - { - return providers; - } - else - { - return new AggregateSourceConfiguration( - new AspNetTriggerSourceConfiguration(supportHeartbeat: false), - new MetricSourceConfiguration(DefaultHeartbeatInterval, new[] { MicrosoftAspNetCoreHostingEventSourceName })).GetProviders(); + }; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs index ad08138c71..ef7ce7317a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs @@ -23,12 +23,10 @@ protected override bool ActivityStart(DateTime timestamp, string activityId) _window.AddDataPoint(timestamp); if (_window.Count >= Settings.RequestCount) { - //Reset trigger - _window.Clear(); return true; } - return base.ActivityStart(timestamp, activityId); + return false; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs index 3ba2e57516..c6d02f0555 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs @@ -29,12 +29,12 @@ protected override bool ActivityStart(DateTime timestamp, string activityId) { _requests.Add(activityId, timestamp); - return base.ActivityStart(timestamp, activityId); + return false; } protected override bool Heartbeat(DateTime timestamp) { - //May may get additional heartbeats based on multiple counters or extra intervals. We only + //May get additional heartbeats based on multiple counters or extra intervals. We only //process the data periodically. if (timestamp - _lastHeartbeatProcessed > _heartBeatInterval) { @@ -67,13 +67,11 @@ protected override bool Heartbeat(DateTime timestamp) if (trigger) { - //Reset trigger - _window.Clear(); return true; } } - return base.Heartbeat(timestamp); + return false; } protected override bool ActivityStop(DateTime timestamp, string activityId, long durationTicks, int statusCode) @@ -91,12 +89,10 @@ protected override bool ActivityStop(DateTime timestamp, string activityId, long if (_window.Count >= Settings.RequestCount) { - //Reset trigger - _window.Clear(); return true; } - return base.ActivityStop(timestamp, activityId, durationTicks, statusCode); + return false; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs index 19bc7ed7a7..d1df25a75e 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs @@ -42,12 +42,10 @@ protected override bool ActivityStop(DateTime timestamp, string activityId, long if (_window.Count >= Settings.RequestCount) { - //Reset trigger - _window.Clear(); return true; } - return base.ActivityStop(timestamp, activityId, durationTicks, statusCode); + return false; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs index 316648b599..e3556522fd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using System.Linq; using System.Text; @@ -15,11 +16,16 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet /// /// Base class for all Asp.net triggers. /// - internal abstract class AspNetTrigger : IAspNetTraceEventTrigger, ITraceEventTrigger where TSettings : AspNetTriggerSettings + internal abstract class AspNetTrigger : ITraceEventTrigger where TSettings : AspNetTriggerSettings { private const string Activity1Start = "Activity1/Start"; private const string Activity1Stop = "Activity1/Stop"; private static readonly Guid CountersGuid = new Guid("{9ded64a4-414c-5251-dcf7-1e4e20c15e70}"); + private static readonly Dictionary> _providerMap = new() + { + {MonitoringSourceConfiguration.DiagnosticSourceEventSource, new[]{ Activity1Start, Activity1Stop } }, + {MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName, new[]{ "EventCounters" } } + }; protected AspNetTrigger(TSettings settings) { @@ -36,14 +42,7 @@ private static void Validate(TSettings settings) Validator.ValidateObject(settings, context, validateAllProperties: true); } - public IReadOnlyDictionary> GetProviderEventMap() - { - return new Dictionary> - { - {MonitoringSourceConfiguration.DiagnosticSourceEventSource, new[]{ Activity1Start, Activity1Stop } }, - {MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName, new[]{ "EventCounters" } } - }; - } + public IReadOnlyDictionary> GetProviderEventMap() => _providerMap; public TSettings Settings { get; } @@ -68,12 +67,12 @@ public bool HasSatisfiedCondition(TraceEvent traceEvent) if (traceEvent.ProviderGuid == CountersGuid) { //Heartbeat only - return ((IAspNetTraceEventTrigger)this).HasSatisfiedCondition(timeStamp, eventType: AspnetTriggerEventType.Heartbeat, activityId: null, path: null, statusCode: null, duration: null); + return HasSatisfiedCondition(timeStamp, eventType: AspnetTriggerEventType.Heartbeat, activityId: null, path: null, statusCode: null, duration: null); } else { - int? statusCode = 200; - long? duration = 50; + int? statusCode = null; + long? duration = null; AspnetTriggerEventType eventType = AspnetTriggerEventType.Start; System.Collections.IList arguments = (System.Collections.IList)traceEvent.PayloadValue(2); @@ -85,13 +84,19 @@ public bool HasSatisfiedCondition(TraceEvent traceEvent) statusCode = int.Parse(ExtractByIndex(arguments, 2)); duration = long.Parse(ExtractByIndex(arguments, 3)); eventType = AspnetTriggerEventType.Stop; + + Debug.Assert(statusCode != null, "Status code cannot be null."); + Debug.Assert(duration != null, "Duration cannot be null."); } - return ((IAspNetTraceEventTrigger)this).HasSatisfiedCondition(timeStamp, eventType, activityId, path, statusCode, duration); + return HasSatisfiedCondition(timeStamp, eventType, activityId, path, statusCode, duration); } } - bool IAspNetTraceEventTrigger.HasSatisfiedCondition(DateTime timestamp, AspnetTriggerEventType eventType, string activityId, string path, int? statusCode, long? duration) + /// + /// This method is to enable testing. + /// + internal bool HasSatisfiedCondition(DateTime timestamp, AspnetTriggerEventType eventType, string activityId, string path, int? statusCode, long? duration) { if (eventType == AspnetTriggerEventType.Heartbeat) { @@ -139,4 +144,11 @@ private static string ExtractByIndex(System.Collections.IList arguments, int ind return (string)values.Last().Value; } } + + internal enum AspnetTriggerEventType + { + Start, + Stop, + Heartbeat + } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs index b2d55fa1eb..8807612244 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTriggerSettings.cs @@ -12,7 +12,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet { internal class AspNetTriggerSettings : IValidatableObject { - public const string SlidingWindowDuration_MaxValue = "01:00:00"; // 1 hour + public const string SlidingWindowDuration_MaxValue = "1.00:00:00"; // 1 day public const string SlidingWindowDuration_MinValue = "00:00:01"; // 1 second /// @@ -25,8 +25,8 @@ internal class AspNetTriggerSettings : IValidatableObject /// The amount of requests that must accumulate in the sliding window and meet the trigger condition. /// Note that requests that do not meet the condition do NOT reset the count. /// - [Range(1, int.MaxValue)] - public int RequestCount { get; set; } + [Range(1, long.MaxValue)] + public long RequestCount { get; set; } /// /// List of request paths to include in the trigger condition, such as "/" and "/About". diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs deleted file mode 100644 index 9c8c7edb79..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/IAspNetTraceEventTrigger.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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.Text; - -namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet -{ - internal enum AspnetTriggerEventType - { - Start, - Stop, - Heartbeat - } - - /// - /// This interface is for testing. It allows the trigger to be callable with a TraceEvent, - /// or with a deconstruction of the TraceEvent payload while running tests. - /// - internal interface IAspNetTraceEventTrigger - { - bool HasSatisfiedCondition(DateTime timestamp, AspnetTriggerEventType eventType, string activityId, string path, int? statusCode, long? duration); - } -} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs index b0c034a260..9aed9279d7 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs @@ -196,12 +196,12 @@ public void TestAspNetStatus() ValidateTriggers(trigger, s1, s2, s3, e3, e1, e2, s4, s5, s6, s7, e5, e4, e6, e7); } - private static void ValidateTriggers(IAspNetTraceEventTrigger requestTrigger, params SimulatedTraceEvent[] events) + private static void ValidateTriggers(AspNetTrigger requestTrigger, params SimulatedTraceEvent[] events) where T: AspNetTriggerSettings { ValidateTriggers(requestTrigger, events.Length - 1, events); } - private static void ValidateTriggers(IAspNetTraceEventTrigger requestTrigger, int triggerIndex, params SimulatedTraceEvent[] events) + private static void ValidateTriggers(AspNetTrigger requestTrigger, int triggerIndex, params SimulatedTraceEvent[] events) where T : AspNetTriggerSettings { for (int i = 0; i < events.Length; i++) { From 71ae7f8eda587258f6a2fff884dc3d58d32c2d14 Mon Sep 17 00:00:00 2001 From: Wiktor Kopec Date: Fri, 17 Sep 2021 14:23:00 -0700 Subject: [PATCH 3/5] StatusCodeRange PR Feedback --- .../AspNet/AspNetRequestStatusTrigger.cs | 16 +----- .../AspNetRequestStatusTriggerSettings.cs | 56 ++++++++++++++----- .../AspNetTriggerUnitTests.cs | 2 +- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs index d1df25a75e..a20c70e432 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs @@ -13,29 +13,15 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet internal sealed class AspNetRequestStatusTrigger : AspNetTrigger { private SlidingWindow _window; - private HashSet _statusCodes = new(); - private List<(int Min, int Max)> _statusCodeRanges = new(); public AspNetRequestStatusTrigger(AspNetRequestStatusTriggerSettings settings) : base(settings) { _window = new SlidingWindow(settings.SlidingWindowDuration); - - foreach(string codeOrRange in settings.StatusCodes) - { - if (codeOrRange.Length == 3) - { - _statusCodes.Add(int.Parse(codeOrRange)); - continue; - } - string[] range = codeOrRange.Split('-'); - _statusCodeRanges.Add((int.Parse(range[0]), int.Parse(range[1]))); - } } protected override bool ActivityStop(DateTime timestamp, string activityId, long durationTicks, int statusCode) { - if (_statusCodes.Contains(statusCode) || - _statusCodeRanges.Any(r => statusCode >= r.Min && statusCode <= r.Max)) + if (Settings.StatusCodes.Any(r => statusCode >= r.Min && statusCode <= r.Max)) { _window.AddDataPoint(timestamp); } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs index e7d47d21e4..a298ef90f8 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs @@ -7,36 +7,64 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; -using System.Text.RegularExpressions; namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet { internal sealed class AspNetRequestStatusTriggerSettings : AspNetTriggerSettings { - private const string StatusRegex = "[1-5][0-9]{2}"; - private static readonly string RangeRegex = FormattableString.Invariant($"^{StatusRegex}(-{StatusRegex})?$"); - /// - /// Specifies the set of status codes for the trigger. This can be individual codes or ranges. - /// E.g. 200;400-500 + /// Specifies the set of status codes for the trigger. + /// E.g. 200-200;400-500 /// [Required] - public string[] StatusCodes { get; set; } + [CustomValidation(typeof(StatusCodeRangeValidator), nameof(StatusCodeRangeValidator.ValidateStatusCodes))] + public StatusCodeRange[] StatusCodes { get; set; } + } + + internal struct StatusCodeRange + { + public StatusCodeRange(int min) : this(min, min) { } + + public StatusCodeRange(int min, int max) + { + Min = min; + Max = max; + } + + public int Min { get; set; } + public int Max { get; set; } + } - public override IEnumerable Validate(ValidationContext validationContext) + public static class StatusCodeRangeValidator + { + public static ValidationResult ValidateStatusCodes(object statusCodes) { - List results = new List(); + StatusCodeRange[] statusCodeRanges = (StatusCodeRange[])statusCodes; + Func validationMembers = () => new[] { nameof(AspNetRequestStatusTriggerSettings.StatusCodes) }; + + if (statusCodeRanges.Length == 0) + { + return new ValidationResult("No ranges specified.", validationMembers()); + } + + Func validateStatusCode = (int statusCode) => statusCode >= 100 && statusCode < 600; - foreach(string statusCode in StatusCodes) + foreach(StatusCodeRange statusCodeRange in statusCodeRanges) { - if (!Regex.IsMatch(statusCode, RangeRegex)) + if (statusCodeRange.Min > statusCodeRange.Max) { - results.Add(new ValidationResult($"'{statusCode}' is not in the correct format.", - new[] { nameof(StatusCodes) })); + return new ValidationResult($"{nameof(StatusCodeRange.Min)} cannot be greater than {nameof(StatusCodeRange.Max)}", + validationMembers()); + } + + if (!validateStatusCode(statusCodeRange.Min) || !validateStatusCode(statusCodeRange.Max)) + { + return new ValidationResult($"Invalid status code", validationMembers()); } } - return results.Concat(base.Validate(validationContext)); + return ValidationResult.Success; } } + } diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs index 9aed9279d7..5f754549d6 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/AspNetTriggerUnitTests.cs @@ -159,7 +159,7 @@ public void TestAspNetStatus() AspNetRequestStatusTriggerSettings settings = new() { RequestCount = 3, - StatusCodes = new[] { "520", "521", "400-500" }, + StatusCodes = new StatusCodeRange[] { new(520), new(521), new(400, 500) }, SlidingWindowDuration = TimeSpan.FromMinutes(1), }; From 826c370853615d2a15120d5b08a095b7a0595d96 Mon Sep 17 00:00:00 2001 From: Wiktor Kopec Date: Fri, 17 Sep 2021 15:15:54 -0700 Subject: [PATCH 4/5] Additional PR feedback --- .../AspNet/AspNetRequestCountTrigger.cs | 7 +------ .../AspNet/AspNetRequestDurationTrigger.cs | 18 ++---------------- .../AspNet/AspNetRequestStatusTrigger.cs | 7 +------ .../AspNetRequestStatusTriggerSettings.cs | 13 +++++-------- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs index ef7ce7317a..e1453788f2 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestCountTrigger.cs @@ -21,12 +21,7 @@ public AspNetRequestCountTrigger(AspNetRequestCountTriggerSettings settings) : b protected override bool ActivityStart(DateTime timestamp, string activityId) { _window.AddDataPoint(timestamp); - if (_window.Count >= Settings.RequestCount) - { - return true; - } - - return false; + return _window.Count >= Settings.RequestCount; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs index c6d02f0555..72e479ef74 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTrigger.cs @@ -40,7 +40,6 @@ protected override bool Heartbeat(DateTime timestamp) { _lastHeartbeatProcessed = timestamp; List requestsToRemove = new(); - bool trigger = false; foreach (KeyValuePair request in _requests) { @@ -52,11 +51,6 @@ protected override bool Heartbeat(DateTime timestamp) //At this point we already deeemed it too slow. We also want to make sure we //clear the cached requests periodically even if they don't finish. requestsToRemove.Add(request.Key); - if (_window.Count >= Settings.RequestCount) - { - trigger = true; - break; - } } } @@ -65,10 +59,7 @@ protected override bool Heartbeat(DateTime timestamp) _requests.Remove(requestId); } - if (trigger) - { - return true; - } + return _window.Count >= Settings.RequestCount; } return false; @@ -87,12 +78,7 @@ protected override bool ActivityStop(DateTime timestamp, string activityId, long _window.AddDataPoint(timestamp); } - if (_window.Count >= Settings.RequestCount) - { - return true; - } - - return false; + return _window.Count >= Settings.RequestCount; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs index a20c70e432..bb1fee5a41 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTrigger.cs @@ -26,12 +26,7 @@ protected override bool ActivityStop(DateTime timestamp, string activityId, long _window.AddDataPoint(timestamp); } - if (_window.Count >= Settings.RequestCount) - { - return true; - } - - return false; + return _window.Count >= Settings.RequestCount; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs index a298ef90f8..592c923fa3 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestStatusTriggerSettings.cs @@ -17,6 +17,7 @@ internal sealed class AspNetRequestStatusTriggerSettings : AspNetTriggerSettings /// E.g. 200-200;400-500 /// [Required] + [MinLength(1)] [CustomValidation(typeof(StatusCodeRangeValidator), nameof(StatusCodeRangeValidator.ValidateStatusCodes))] public StatusCodeRange[] StatusCodes { get; set; } } @@ -37,15 +38,11 @@ public StatusCodeRange(int min, int max) public static class StatusCodeRangeValidator { + private static readonly string[] _validationMembers = new[] { nameof(AspNetRequestStatusTriggerSettings.StatusCodes)}; + public static ValidationResult ValidateStatusCodes(object statusCodes) { StatusCodeRange[] statusCodeRanges = (StatusCodeRange[])statusCodes; - Func validationMembers = () => new[] { nameof(AspNetRequestStatusTriggerSettings.StatusCodes) }; - - if (statusCodeRanges.Length == 0) - { - return new ValidationResult("No ranges specified.", validationMembers()); - } Func validateStatusCode = (int statusCode) => statusCode >= 100 && statusCode < 600; @@ -54,12 +51,12 @@ public static ValidationResult ValidateStatusCodes(object statusCodes) if (statusCodeRange.Min > statusCodeRange.Max) { return new ValidationResult($"{nameof(StatusCodeRange.Min)} cannot be greater than {nameof(StatusCodeRange.Max)}", - validationMembers()); + _validationMembers); } if (!validateStatusCode(statusCodeRange.Min) || !validateStatusCode(statusCodeRange.Max)) { - return new ValidationResult($"Invalid status code", validationMembers()); + return new ValidationResult($"Invalid status code", _validationMembers); } } From 06e3c1a0bb4eae701dbb6010db2ab520d724d4b6 Mon Sep 17 00:00:00 2001 From: Wiktor Kopec Date: Mon, 20 Sep 2021 14:17:12 -0700 Subject: [PATCH 5/5] PR feedback --- .../AspNetTriggerSourceConfiguration.cs | 2 +- .../HttpRequestSourceConfiguration.cs | 2 +- .../MonitoringSourceConfiguration.cs | 10 ++++++++++ .../AspNetRequestDurationTriggerSettings.cs | 2 +- .../Triggers/AspNet/AspNetTrigger.cs | 17 ++++++++--------- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs index 186dbc2007..f27d97c2b4 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/AspNetTriggerSourceConfiguration.cs @@ -57,7 +57,7 @@ public override IList GetProviders() return new[] { new EventPipeProvider(DiagnosticSourceEventSource, - keywords: 0x1 | 0x2, + keywords: DiagnosticSourceEventSourceEvents | DiagnosticSourceEventSourceMessages, eventLevel: EventLevel.Verbose, arguments: new Dictionary { diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/HttpRequestSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/HttpRequestSourceConfiguration.cs index 595ec71a68..904e8d843f 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/HttpRequestSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/HttpRequestSourceConfiguration.cs @@ -57,7 +57,7 @@ public override IList GetProviders() { // Diagnostic source events new EventPipeProvider(DiagnosticSourceEventSource, - keywords: 0x1 | 0x2, + keywords: DiagnosticSourceEventSourceEvents | DiagnosticSourceEventSourceMessages, eventLevel: EventLevel.Verbose, arguments: new Dictionary { diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs index 2bab551996..2fc5c24d80 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Configuration/MonitoringSourceConfiguration.cs @@ -9,6 +9,16 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe { public abstract class MonitoringSourceConfiguration { + /// + /// Indicates diagnostics messages from DiagnosticSourceEventSource should be included. + /// + public const long DiagnosticSourceEventSourceMessages = 0x1; + + /// + /// Indicates that all events from all diagnostic sources should be forwarded to the EventSource using the 'Event' event. + /// + public const long DiagnosticSourceEventSourceEvents = 0x2; + public const string MicrosoftExtensionsLoggingProviderName = "Microsoft-Extensions-Logging"; public const string SystemRuntimeEventSourceName = "System.Runtime"; public const string MicrosoftAspNetCoreHostingEventSourceName = "Microsoft.AspNetCore.Hosting"; diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs index cc39a544d3..56847ec4ed 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetRequestDurationTriggerSettings.cs @@ -13,7 +13,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.AspNet internal sealed class AspNetRequestDurationTriggerSettings : AspNetTriggerSettings { public const string RequestDuration_MaxValue = "01:00:00"; // 1 hour - public const string RequestDuration_MinValue = "00:00:01"; // 1 second + public const string RequestDuration_MinValue = "00:00:00"; // No minimum /// /// The minimum duration of the request to be considered slow. diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs index e3556522fd..57c7558076 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Triggers/AspNet/AspNetTrigger.cs @@ -20,11 +20,11 @@ internal abstract class AspNetTrigger : ITraceEventTrigger where TSet { private const string Activity1Start = "Activity1/Start"; private const string Activity1Stop = "Activity1/Stop"; - private static readonly Guid CountersGuid = new Guid("{9ded64a4-414c-5251-dcf7-1e4e20c15e70}"); + private static readonly Guid MicrosoftAspNetCoreHostingGuid = new Guid("{adb401e1-5296-51f8-c125-5fda75826144}"); private static readonly Dictionary> _providerMap = new() { - {MonitoringSourceConfiguration.DiagnosticSourceEventSource, new[]{ Activity1Start, Activity1Stop } }, - {MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName, new[]{ "EventCounters" } } + { MonitoringSourceConfiguration.DiagnosticSourceEventSource, new[]{ Activity1Start, Activity1Stop } }, + { MonitoringSourceConfiguration.MicrosoftAspNetCoreHostingEventSourceName, new[]{ "EventCounters" } } }; protected AspNetTrigger(TSettings settings) @@ -64,12 +64,7 @@ public bool HasSatisfiedCondition(TraceEvent traceEvent) //We deconstruct the TraceEvent data to make it easy to write tests DateTime timeStamp = traceEvent.TimeStamp; - if (traceEvent.ProviderGuid == CountersGuid) - { - //Heartbeat only - return HasSatisfiedCondition(timeStamp, eventType: AspnetTriggerEventType.Heartbeat, activityId: null, path: null, statusCode: null, duration: null); - } - else + if (traceEvent.ProviderGuid == MicrosoftAspNetCoreHostingGuid) { int? statusCode = null; long? duration = null; @@ -91,6 +86,10 @@ public bool HasSatisfiedCondition(TraceEvent traceEvent) return HasSatisfiedCondition(timeStamp, eventType, activityId, path, statusCode, duration); } + + //Heartbeat only + return HasSatisfiedCondition(timeStamp, eventType: AspnetTriggerEventType.Heartbeat, activityId: null, path: null, statusCode: null, duration: null); + } ///