Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial trace event trigger implementation and tests. #2508

Merged
merged 6 commits into from
Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<MicrosoftExtensionsLoggingEventSourceVersion>5.0.1</MicrosoftExtensionsLoggingEventSourceVersion>
<SystemCommandLineVersion>2.0.0-beta1.20468.1</SystemCommandLineVersion>
<SystemCommandLineRenderingVersion>2.0.0-beta1.20074.1</SystemCommandLineRenderingVersion>
<SystemComponentModelAnnotationsVersion>5.0.0</SystemComponentModelAnnotationsVersion>
<SystemMemoryVersion>4.5.4</SystemMemoryVersion>
<SystemTextEncodingsWebVersion>4.7.2</SystemTextEncodingsWebVersion>
<SystemTextJsonVersion>4.7.1</SystemTextJsonVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe
internal sealed class CounterFilter
{
private Dictionary<string, List<string>> _enabledCounters;
private int _intervalMilliseconds;

public static CounterFilter AllCounters { get; } = new CounterFilter();
public static CounterFilter AllCounters(int counterIntervalSeconds)
=> new CounterFilter(counterIntervalSeconds);

public CounterFilter()
public CounterFilter(int intervalSeconds)
{
//Provider names are not case sensitive, but counter names are.
_enabledCounters = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
_intervalMilliseconds = intervalSeconds * 1000;
}

// Called when we want to enable all counters under a provider name.
Expand All @@ -33,8 +36,12 @@ public void AddFilter(string providerName, string[] counters)

public IEnumerable<string> GetProviders() => _enabledCounters.Keys;

public bool IsIncluded(string providerName, string counterName)
public bool IsIncluded(string providerName, string counterName, int interval)
{
if (_intervalMilliseconds != interval)
{
return false;
}
jander-msft marked this conversation as resolved.
Show resolved Hide resolved
if (_enabledCounters.Count == 0)
{
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ public EventCounterPipeline(DiagnosticsClient client,

if (settings.CounterGroups.Length > 0)
{
_filter = new CounterFilter();
_filter = new CounterFilter(CounterIntervalSeconds);
foreach (var counterGroup in settings.CounterGroups)
{
_filter.AddFilter(counterGroup.ProviderName, counterGroup.CounterNames);
}
}
else
{
_filter = CounterFilter.AllCounters;
_filter = CounterFilter.AllCounters(CounterIntervalSeconds);
}
}

Expand All @@ -49,56 +49,8 @@ protected override async Task OnEventSourceAvailable(EventPipeEventSource eventS
{
try
{
// Metrics
if (traceEvent.EventName.Equals("EventCounters"))
if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload counterPayload))
{
IDictionary<string, object> payloadVal = (IDictionary<string, object>)(traceEvent.PayloadValue(0));
IDictionary<string, object> payloadFields = (IDictionary<string, object>)(payloadVal["Payload"]);

//Make sure we are part of the requested series. If multiple clients request metrics, all of them get the metrics.
string series = payloadFields["Series"].ToString();
if (GetInterval(series) != CounterIntervalSeconds * 1000)
{
return;
}

string counterName = payloadFields["Name"].ToString();
if (!_filter.IsIncluded(traceEvent.ProviderName, counterName))
{
return;
}

float intervalSec = (float)payloadFields["IntervalSec"];
string displayName = payloadFields["DisplayName"].ToString();
string displayUnits = payloadFields["DisplayUnits"].ToString();
double value = 0;
CounterType counterType = CounterType.Metric;

if (payloadFields["CounterType"].Equals("Mean"))
{
value = (double)payloadFields["Mean"];
}
else if (payloadFields["CounterType"].Equals("Sum"))
{
counterType = CounterType.Rate;
value = (double)payloadFields["Increment"];
if (string.IsNullOrEmpty(displayUnits))
{
displayUnits = "count";
}
//TODO Should we make these /sec like the dotnet-counters tool?
}

// Note that dimensional data such as pod and namespace are automatically added in prometheus and azure monitor scenarios.
// We no longer added it here.
var counterPayload = new CounterPayload(traceEvent.TimeStamp,
traceEvent.ProviderName,
counterName, displayName,
displayUnits,
value,
counterType,
intervalSec);

ExecuteCounterLoggerAction((metricLogger) => metricLogger.Log(counterPayload));
}
}
Expand All @@ -118,17 +70,6 @@ protected override async Task OnEventSourceAvailable(EventPipeEventSource eventS
ExecuteCounterLoggerAction((metricLogger) => metricLogger.PipelineStopped());
}

private static int GetInterval(string series)
{
const string comparison = "Interval=";
int interval = 0;
if (series.StartsWith(comparison, StringComparison.OrdinalIgnoreCase))
{
int.TryParse(series.Substring(comparison.Length), out interval);
}
return interval;
}

private void ExecuteCounterLoggerAction(Action<ICountersLogger> action)
{
foreach (ICountersLogger logger in _loggers)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Diagnostics.Tracing;
using System;
using System.Collections.Generic;

namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal static class TraceEventExtensions
{
public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, out ICounterPayload payload)
{
payload = null;

if ("EventCounters".Equals(traceEvent.EventName))
{
IDictionary<string, object> payloadVal = (IDictionary<string, object>)(traceEvent.PayloadValue(0));
IDictionary<string, object> payloadFields = (IDictionary<string, object>)(payloadVal["Payload"]);

//Make sure we are part of the requested series. If multiple clients request metrics, all of them get the metrics.
string series = payloadFields["Series"].ToString();
string counterName = payloadFields["Name"].ToString();
if (!filter.IsIncluded(traceEvent.ProviderName, counterName, GetInterval(series)))
{
return false;
}

float intervalSec = (float)payloadFields["IntervalSec"];
string displayName = payloadFields["DisplayName"].ToString();
string displayUnits = payloadFields["DisplayUnits"].ToString();
double value = 0;
CounterType counterType = CounterType.Metric;

if (payloadFields["CounterType"].Equals("Mean"))
{
value = (double)payloadFields["Mean"];
}
else if (payloadFields["CounterType"].Equals("Sum"))
{
counterType = CounterType.Rate;
value = (double)payloadFields["Increment"];
if (string.IsNullOrEmpty(displayUnits))
{
displayUnits = "count";
}
//TODO Should we make these /sec like the dotnet-counters tool?
jander-msft marked this conversation as resolved.
Show resolved Hide resolved
}

// Note that dimensional data such as pod and namespace are automatically added in prometheus and azure monitor scenarios.
// We no longer added it here.
payload = new CounterPayload(
traceEvent.TimeStamp,
traceEvent.ProviderName,
counterName, displayName,
displayUnits,
value,
counterType,
intervalSec);
return true;
}

return false;
}

private static int GetInterval(string series)
{
const string comparison = "Interval=";
int interval = 0;
if (series.StartsWith(comparison, StringComparison.OrdinalIgnoreCase))
{
int.TryParse(series.Substring(comparison.Length), out interval);
}
return interval;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="$(MicrosoftDiagnosticsTracingTraceEventVersion)" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="$(MicrosoftBclAsyncInterfacesVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingVersion)" />
<PackageReference Include="System.ComponentModel.Annotations" Version="$(SystemComponentModelAnnotationsVersion)" />
<PackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebVersion)" />
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Diagnostics.Tracing;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter
jander-msft marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Trigger that detects when the specified event source counter value is held
/// above, below, or between threshold values for a specified duration of time.
/// </summary>
internal sealed class EventCounterTrigger :
ITraceEventTrigger
{
private readonly CounterFilter _filter;
private readonly EventCounterTriggerImpl _impl;
private readonly string _providerName;

public EventCounterTrigger(EventCounterTriggerSettings settings)
{
if (null == settings)
{
throw new ArgumentNullException(nameof(settings));
}

Validate(settings);

_filter = new CounterFilter(settings.CounterIntervalSeconds);
_filter.AddFilter(settings.ProviderName, new string[] { settings.CounterName });

_impl = new(settings);
jander-msft marked this conversation as resolved.
Show resolved Hide resolved

_providerName = settings.ProviderName;
}

public IDictionary<string, IEnumerable<string>> GetProviderEventMap()
{
return new Dictionary<string, IEnumerable<string>>()
jander-msft marked this conversation as resolved.
Show resolved Hide resolved
{
{ _providerName, new string[] { "EventCounters" } }
};
}

public bool HasSatisfiedCondition(TraceEvent traceEvent)
{
// Filter to the counter of interest before forwarding to the implementation
if (traceEvent.TryGetCounterPayload(_filter, out ICounterPayload payload))
{
return _impl.HasSatisfiedCondition(payload);
}
return false;
}

public static MonitoringSourceConfiguration CreateConfiguration(EventCounterTriggerSettings settings)
{
Validate(settings);

return new MetricSourceConfiguration(settings.CounterIntervalSeconds, new string[] { settings.ProviderName });
}

private static void Validate(EventCounterTriggerSettings settings)
{
ValidationContext context = new(settings);
Validator.ValidateObject(settings, context, validateAllProperties: true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Runtime;

namespace Microsoft.Diagnostics.Monitoring.EventPipe.Triggers.EventCounter
{
/// <summary>
/// The trigger factory for the <see cref="EventCounterTrigger"/>.
/// </summary>
internal sealed class EventCounterTriggerFactory :
ITraceEventTriggerFactory<EventCounterTriggerSettings>
{
public ITraceEventTrigger Create(EventCounterTriggerSettings settings)
{
return new EventCounterTrigger(settings);
}
}
}
Loading