Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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;
}

/// <summary>
/// Filter string for trigger data. Note that even though some triggers use start OR stop,
/// collecting just one causes unusual behavior in data collection.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What sort of unusual behavior?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specific behavior is that I get a set of start events (for example) but when I request the data again I get no data at all.

/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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<EventPipeProvider> GetProviders()
{
if (_supportHeartbeat)
{
return new AggregateSourceConfiguration(
new AspNetTriggerSourceConfiguration(supportHeartbeat: false),
new MetricSourceConfiguration(DefaultHeartbeatInterval, new[] { MicrosoftAspNetCoreHostingEventSourceName })).GetProviders();

}
else
{
return new[]
{
new EventPipeProvider(DiagnosticSourceEventSource,
keywords: DiagnosticSourceEventSourceEvents | DiagnosticSourceEventSourceMessages,
eventLevel: EventLevel.Verbose,
arguments: new Dictionary<string,string>
{
{ "FilterAndPayloadSpecs", DiagnosticFilterString }
})
};
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public override IList<EventPipeProvider> GetProviders()
{
// Diagnostic source events
new EventPipeProvider(DiagnosticSourceEventSource,
keywords: 0x1 | 0x2,
keywords: DiagnosticSourceEventSourceEvents | DiagnosticSourceEventSourceMessages,
eventLevel: EventLevel.Verbose,
arguments: new Dictionary<string,string>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
public abstract class MonitoringSourceConfiguration
{
/// <summary>
/// Indicates diagnostics messages from DiagnosticSourceEventSource should be included.
/// </summary>
public const long DiagnosticSourceEventSourceMessages = 0x1;

/// <summary>
/// Indicates that all events from all diagnostic sources should be forwarded to the EventSource using the 'Event' event.
/// </summary>
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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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<AspNetRequestCountTriggerSettings>
{
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);
return _window.Count >= Settings.RequestCount;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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<AspNetRequestDurationTriggerSettings>
{
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<string, DateTime> _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 false;
}

protected override bool Heartbeat(DateTime timestamp)
{
//May get additional heartbeats based on multiple counters or extra intervals. We only
//process the data periodically.
if (timestamp - _lastHeartbeatProcessed > _heartBeatInterval)
{
_lastHeartbeatProcessed = timestamp;
List<string> requestsToRemove = new();

foreach (KeyValuePair<string, DateTime> 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);
}
}

foreach(string requestId in requestsToRemove)
{
_requests.Remove(requestId);
}

return _window.Count >= Settings.RequestCount;
}

return false;
}

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);
}

return _window.Count >= Settings.RequestCount;
}
}
}
Original file line number Diff line number Diff line change
@@ -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:00"; // No minimum

/// <summary>
/// The minimum duration of the request to be considered slow.
/// </summary>
[Range(typeof(TimeSpan), RequestDuration_MinValue, RequestDuration_MaxValue)]
public TimeSpan RequestDuration { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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<AspNetRequestStatusTriggerSettings>
{
private SlidingWindow _window;

public AspNetRequestStatusTrigger(AspNetRequestStatusTriggerSettings settings) : base(settings)
{
_window = new SlidingWindow(settings.SlidingWindowDuration);
}

protected override bool ActivityStop(DateTime timestamp, string activityId, long durationTicks, int statusCode)
{
if (Settings.StatusCodes.Any(r => statusCode >= r.Min && statusCode <= r.Max))
{
_window.AddDataPoint(timestamp);
}

return _window.Count >= Settings.RequestCount;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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 AspNetRequestStatusTriggerSettings : AspNetTriggerSettings
{
/// <summary>
/// Specifies the set of status codes for the trigger.
/// E.g. 200-200;400-500
/// </summary>
[Required]
[MinLength(1)]
[CustomValidation(typeof(StatusCodeRangeValidator), nameof(StatusCodeRangeValidator.ValidateStatusCodes))]
public StatusCodeRange[] StatusCodes { get; set; }
Comment thread
wiktork marked this conversation as resolved.
}

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 static class StatusCodeRangeValidator
{
private static readonly string[] _validationMembers = new[] { nameof(AspNetRequestStatusTriggerSettings.StatusCodes)};

public static ValidationResult ValidateStatusCodes(object statusCodes)
{
StatusCodeRange[] statusCodeRanges = (StatusCodeRange[])statusCodes;

Func<int, bool> validateStatusCode = (int statusCode) => statusCode >= 100 && statusCode < 600;

foreach(StatusCodeRange statusCodeRange in statusCodeRanges)
{
if (statusCodeRange.Min > statusCodeRange.Max)
{
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 ValidationResult.Success;
}
}

}
Loading