public sealed class MetricSourceConfiguration : MonitoringSourceConfiguration
{
+ private const string SharedSessionId = "SHARED";
+
private readonly IList<EventPipeProvider> _eventPipeProviders;
+ public string ClientId { get; private set; }
public string SessionId { get; private set; }
public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable<string> eventCounterProviderNames)
{
}
- public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable<MetricEventPipeProvider> providers, int maxHistograms = 20, int maxTimeSeries = 1000)
+ public MetricSourceConfiguration(float metricIntervalSeconds, IEnumerable<MetricEventPipeProvider> providers, int maxHistograms = 20, int maxTimeSeries = 1000, bool useSharedSession = false)
{
if (providers == null)
{
const long TimeSeriesValuesEventKeyword = 0x2;
string metrics = string.Join(',', meterProviders.Select(p => p.Provider));
- SessionId = Guid.NewGuid().ToString();
+ ClientId = Guid.NewGuid().ToString();
+
+ // Shared Session Id was added in 8.0 - older runtimes will not properly support it.
+ SessionId = useSharedSession ? SharedSessionId : Guid.NewGuid().ToString();
EventPipeProvider metricsEventSourceProvider =
new(MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName, EventLevel.Informational, TimeSeriesValuesEventKeyword,
{ "Metrics", metrics },
{ "RefreshInterval", metricIntervalSeconds.ToString(CultureInfo.InvariantCulture) },
{ "MaxTimeSeries", maxTimeSeries.ToString(CultureInfo.InvariantCulture) },
- { "MaxHistograms", maxHistograms.ToString(CultureInfo.InvariantCulture) }
+ { "MaxHistograms", maxHistograms.ToString(CultureInfo.InvariantCulture) },
+ { "ClientId", ClientId }
}
);
{
private readonly IEnumerable<ICountersLogger> _loggers;
private readonly CounterFilter _filter;
+ private string _clientId;
private string _sessionId;
public MetricsPipeline(DiagnosticsClient client,
IntervalSeconds = counterGroup.IntervalSeconds,
Type = (MetricType)counterGroup.Type
}),
- Settings.MaxHistograms, Settings.MaxTimeSeries);
+ Settings.MaxHistograms, Settings.MaxTimeSeries, useSharedSession: Settings.UseSharedSession);
+ _clientId = config.ClientId;
_sessionId = config.SessionId;
return config;
eventSource.Dynamic.All += traceEvent => {
try
{
- if (traceEvent.TryGetCounterPayload(_filter, _sessionId, out ICounterPayload counterPayload))
+ if (traceEvent.TryGetCounterPayload(_filter, _sessionId, _clientId, out ICounterPayload counterPayload))
{
ExecuteCounterLoggerAction((metricLogger) => metricLogger.Log(counterPayload));
}
public int MaxHistograms { get; set; }
public int MaxTimeSeries { get; set; }
+
+ public bool UseSharedSession { get; set; }
}
[Flags]
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Text;
using Microsoft.Diagnostics.Tracing;
namespace Microsoft.Diagnostics.Monitoring.EventPipe
{
internal static class TraceEventExtensions
{
- public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, string sessionId, out ICounterPayload payload)
+ private static HashSet<string> inactiveSharedSessions = new(StringComparer.OrdinalIgnoreCase);
+
+ public static bool TryGetCounterPayload(this TraceEvent traceEvent, CounterFilter filter, string sessionId, string clientId, out ICounterPayload payload)
{
payload = null;
return true;
}
- if (sessionId != null && MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName.Equals(traceEvent.ProviderName))
+ if (clientId != null && !inactiveSharedSessions.Contains(clientId) && MonitoringSourceConfiguration.SystemDiagnosticsMetricsProviderName.Equals(traceEvent.ProviderName))
{
if (traceEvent.EventName == "BeginInstrumentReporting")
{
{
HandleMultipleSessionsNotSupportedError(traceEvent, sessionId, out payload);
}
+ else if (traceEvent.EventName == "MultipleSessionsConfiguredIncorrectlyError")
+ {
+ HandleMultipleSessionsConfiguredIncorrectlyError(traceEvent, clientId, out payload);
+ }
return payload != null;
}
string payloadSessionId = (string)obj.PayloadValue(0);
string error = (string)obj.PayloadValue(1);
- if (sessionId != payloadSessionId)
+ if (payloadSessionId != sessionId)
{
return;
}
}
else
{
- string errorMessage = "Error: Another metrics collection session is already in progress for the target process, perhaps from another tool? " + Environment.NewLine +
+ string errorMessage = "Error: Another metrics collection session is already in progress for the target process." + Environment.NewLine +
"Concurrent sessions are not supported.";
payload = new ErrorPayload(errorMessage, obj.TimeStamp);
}
}
+ internal static bool TryCreateSharedSessionConfiguredIncorrectlyMessage(TraceEvent obj, string clientId, out string message)
+ {
+ message = string.Empty;
+
+ string payloadSessionId = (string)obj.PayloadValue(0);
+
+ if (payloadSessionId != clientId)
+ {
+ // If our session is not the one that is running then the error is not for us,
+ // it is for some other session that came later
+ return false;
+ }
+
+ string expectedMaxHistograms = (string)obj.PayloadValue(1);
+ string actualMaxHistograms = (string)obj.PayloadValue(2);
+ string expectedMaxTimeSeries = (string)obj.PayloadValue(3);
+ string actualMaxTimeSeries = (string)obj.PayloadValue(4);
+ string expectedRefreshInterval = (string)obj.PayloadValue(5);
+ string actualRefreshInterval = (string)obj.PayloadValue(6);
+
+ StringBuilder errorMessage = new("Error: Another shared metrics collection session is already in progress for the target process." + Environment.NewLine +
+ "To enable this metrics session alongside the existing session, update the following values:" + Environment.NewLine);
+
+ if (expectedMaxHistograms != actualMaxHistograms)
+ {
+ errorMessage.Append($"MaxHistograms: {expectedMaxHistograms}" + Environment.NewLine);
+ }
+ if (expectedMaxTimeSeries != actualMaxTimeSeries)
+ {
+ errorMessage.Append($"MaxTimeSeries: {expectedMaxTimeSeries}" + Environment.NewLine);
+ }
+ if (expectedRefreshInterval != actualRefreshInterval)
+ {
+ errorMessage.Append($"IntervalSeconds: {expectedRefreshInterval}" + Environment.NewLine);
+ }
+
+ message = errorMessage.ToString();
+
+ return true;
+ }
+
+ private static void HandleMultipleSessionsConfiguredIncorrectlyError(TraceEvent obj, string clientId, out ICounterPayload payload)
+ {
+ payload = null;
+
+ if (TryCreateSharedSessionConfiguredIncorrectlyMessage(obj, clientId, out string message))
+ {
+ payload = new ErrorPayload(message.ToString(), obj.TimeStamp);
+
+ inactiveSharedSessions.Add(clientId);
+ }
+ }
+
private static void HandleObservableInstrumentCallbackError(TraceEvent obj, string sessionId, out ICounterPayload payload)
{
payload = null;
<ItemGroup>
<InternalsVisibleTo Include="dotnet-monitor" />
+ <InternalsVisibleTo Include="dotnet-counters" />
<InternalsVisibleTo Include="Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Diagnostics.Monitoring.WebApi" />
<InternalsVisibleTo Include="Microsoft.Diagnostics.Monitoring.Tool.UnitTests" />
public bool HasSatisfiedCondition(TraceEvent traceEvent)
{
// Filter to the counter of interest before forwarding to the implementation
- if (traceEvent.TryGetCounterPayload(_filter, null, out ICounterPayload payload))
+ if (traceEvent.TryGetCounterPayload(_filter, null, null, out ICounterPayload payload))
{
return _impl.HasSatisfiedCondition(payload);
}
private readonly CounterFilter _filter;
private readonly SystemDiagnosticsMetricsTriggerImpl _impl;
private readonly string _meterName;
+ private readonly string _clientId;
private readonly string _sessionId;
public SystemDiagnosticsMetricsTrigger(SystemDiagnosticsMetricsTriggerSettings settings)
_meterName = settings.MeterName;
+ _clientId = settings.ClientId;
+
_sessionId = settings.SessionId;
}
public bool HasSatisfiedCondition(TraceEvent traceEvent)
{
// Filter to the counter of interest before forwarding to the implementation
- if (traceEvent.TryGetCounterPayload(_filter, _sessionId, out ICounterPayload payload))
+ if (traceEvent.TryGetCounterPayload(_filter, _sessionId, _clientId, out ICounterPayload payload))
{
return _impl.HasSatisfiedCondition(payload);
}
settings.CounterIntervalSeconds,
MetricSourceConfiguration.CreateProviders(new string[] { settings.MeterName }, MetricType.Meter),
settings.MaxHistograms,
- settings.MaxTimeSeries);
+ settings.MaxTimeSeries,
+ useSharedSession: settings.UseSharedSession);
+ settings.ClientId = config.ClientId;
settings.SessionId = config.SessionId;
return config;
public int MaxTimeSeries { get; set; }
+ public string ClientId { get; set; }
+
public string SessionId { get; set; }
+ public bool UseSharedSession { get; set; }
+
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
{
return SharedTriggerSettingsValidation.Validate(GreaterThan, LessThan);
return processInfo;
}
+ internal bool TryGetProcessClrVersion(out Version version)
+ {
+ version = null;
+ if (string.IsNullOrEmpty(ClrProductVersionString))
+ {
+ return false;
+ }
+
+ // The version is of the SemVer2 form: <major>.<minor>.<patch>[-<prerelease>][+<metadata>]
+ // Remove the prerelease and metadata version information before parsing.
+
+ ReadOnlySpan<char> versionSpan = ClrProductVersionString.AsSpan();
+ int metadataIndex = versionSpan.IndexOf('+');
+ if (-1 == metadataIndex)
+ {
+ metadataIndex = versionSpan.Length;
+ }
+
+ ReadOnlySpan<char> noMetadataVersion = versionSpan.Slice(0, metadataIndex);
+ int prereleaseIndex = noMetadataVersion.IndexOf('-');
+ if (-1 == prereleaseIndex)
+ {
+ prereleaseIndex = metadataIndex;
+ }
+
+ return Version.TryParse(noMetadataVersion.Slice(0, prereleaseIndex).ToString(), out version);
+ }
+
private static ProcessInfo ParseCommon2(byte[] payload, ref int index)
{
ProcessInfo processInfo = ParseCommon(payload, ref index);
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Diagnostics.Monitoring.EventPipe;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tools.Counters.Exporters;
using Microsoft.Diagnostics.Tracing;
public class CounterMonitor
{
private const int BufferDelaySecs = 1;
+ private const string SharedSessionId = "SHARED"; // This should be identical to the one used by dotnet-monitor in MetricSourceConfiguration.cs
+ private static HashSet<string> inactiveSharedSessions = new(StringComparer.OrdinalIgnoreCase);
+ private string _sessionId;
private int _processId;
private int _interval;
private CounterSet _counterList;
private bool _resumeRuntime;
private DiagnosticsClient _diagnosticsClient;
private EventPipeSession _session;
- private readonly string _metricsEventSourceSessionId;
+ private readonly string _clientId;
private int _maxTimeSeries;
private int _maxHistograms;
private TimeSpan _duration;
public CounterMonitor()
{
_pauseCmdSet = false;
- _metricsEventSourceSessionId = Guid.NewGuid().ToString();
+ _clientId = Guid.NewGuid().ToString();
+
_shouldExit = new TaskCompletionSource<int>();
}
// There's a potential race here between the two tasks but not a huge deal if we miss by one event.
_renderer.ToggleStatus(_pauseCmdSet);
- if (obj.ProviderName == "System.Diagnostics.Metrics")
+ // If a session received a MultipleSessionsConfiguredIncorrectlyError, ignore future shared events
+ if (obj.ProviderName == "System.Diagnostics.Metrics" && !inactiveSharedSessions.Contains(_clientId))
{
if (obj.EventName == "BeginInstrumentReporting")
{
{
HandleMultipleSessionsNotSupportedError(obj);
}
+ else if (obj.EventName == "MultipleSessionsConfiguredIncorrectlyError")
+ {
+ HandleMultipleSessionsConfiguredIncorrectlyError(obj);
+ }
}
else if (obj.EventName == "EventCounters")
{
string sessionId = (string)obj.PayloadValue(0);
string meterName = (string)obj.PayloadValue(1);
// string instrumentName = (string)obj.PayloadValue(3);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId)
{
return;
}
string unit = (string)obj.PayloadValue(4);
string tags = (string)obj.PayloadValue(5);
string rateText = (string)obj.PayloadValue(6);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId || !Filter(meterName, instrumentName))
{
return;
}
string unit = (string)obj.PayloadValue(4);
string tags = (string)obj.PayloadValue(5);
string lastValueText = (string)obj.PayloadValue(6);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId || !Filter(meterName, instrumentName))
{
return;
}
string tags = (string)obj.PayloadValue(5);
//string rateText = (string)obj.PayloadValue(6); // Not currently using rate for UpDownCounters.
string valueText = (string)obj.PayloadValue(7);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId || !Filter(meterName, instrumentName))
{
return;
}
string unit = (string)obj.PayloadValue(4);
string tags = (string)obj.PayloadValue(5);
string quantilesText = (string)obj.PayloadValue(6);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId || !Filter(meterName, instrumentName))
{
return;
}
private void HandleHistogramLimitReached(TraceEvent obj)
{
string sessionId = (string)obj.PayloadValue(0);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _clientId)
{
return;
}
private void HandleTimeSeriesLimitReached(TraceEvent obj)
{
string sessionId = (string)obj.PayloadValue(0);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId)
{
return;
}
{
string sessionId = (string)obj.PayloadValue(0);
string error = (string)obj.PayloadValue(1);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId)
{
return;
}
{
string sessionId = (string)obj.PayloadValue(0);
string error = (string)obj.PayloadValue(1);
- if (sessionId != _metricsEventSourceSessionId)
+ if (sessionId != _sessionId)
{
return;
}
private void HandleMultipleSessionsNotSupportedError(TraceEvent obj)
{
string runningSessionId = (string)obj.PayloadValue(0);
- if (runningSessionId == _metricsEventSourceSessionId)
+ if (runningSessionId == _sessionId)
{
// If our session is the one that is running then the error is not for us,
// it is for some other session that came later
return;
}
_renderer.SetErrorText(
- "Error: Another metrics collection session is already in progress for the target process, perhaps from another tool?" + Environment.NewLine +
+ "Error: Another metrics collection session is already in progress for the target process." + Environment.NewLine +
"Concurrent sessions are not supported.");
_shouldExit.TrySetResult((int)ReturnCode.SessionCreationError);
}
+ private void HandleMultipleSessionsConfiguredIncorrectlyError(TraceEvent obj)
+ {
+ if (TraceEventExtensions.TryCreateSharedSessionConfiguredIncorrectlyMessage(obj, _clientId, out string message))
+ {
+ _renderer.SetErrorText(message);
+ inactiveSharedSessions.Add(_clientId);
+ _shouldExit.TrySetResult((int)ReturnCode.SessionCreationError);
+ }
+ }
+
private static KeyValuePair<double, double>[] ParseQuantiles(string quantileList)
{
string[] quantileParts = quantileList.Split(';', StringSplitOptions.RemoveEmptyEntries);
metrics.Append(string.Join(',', providerCounters));
}
}
+
+ // Shared Session Id was added in 8.0 - older runtimes will not properly support it.
+ _sessionId = Guid.NewGuid().ToString();
+ if (_diagnosticsClient.GetProcessInfo().TryGetProcessClrVersion(out Version version))
+ {
+ _sessionId = version.Major >= 8 ? SharedSessionId : _sessionId;
+ }
+
EventPipeProvider metricsEventSourceProvider =
new("System.Diagnostics.Metrics", EventLevel.Informational, TimeSeriesValues,
new Dictionary<string, string>()
{
- { "SessionId", _metricsEventSourceSessionId },
+ { "SessionId", _sessionId },
{ "Metrics", metrics.ToString() },
{ "RefreshInterval", _interval.ToString() },
{ "MaxTimeSeries", _maxTimeSeries.ToString() },
- { "MaxHistograms", _maxHistograms.ToString() }
+ { "MaxHistograms", _maxHistograms.ToString() },
+ { "ClientId", _clientId }
}
);
return eventCounterProviders.Append(metricsEventSourceProvider).ToArray();
}
+ private bool Filter(string meterName, string instrumentName)
+ {
+ return _counterList.GetCounters(meterName).Contains(instrumentName) || _counterList.IncludesAllCounters(meterName);
+ }
+
private Task<int> Start()
{
EventPipeProvider[] providers = GetEventPipeProviders();
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.NETCore.Client\Microsoft.Diagnostics.NETCore.Client.csproj" />
+ <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostics.Monitoring.EventPipe\Microsoft.Diagnostics.Monitoring.EventPipe.csproj" />
</ItemGroup>
<ItemGroup>